- メモ帳アプリ 2日目のゴール
- 今日の全体ステップ
- 1日目のベース(HTML)はそのまま使う
- 状態と DOM 参照の定義(1日目と同じ+α)
- localStorage の保存・読み込み(復習+そのまま使う)
- メモ追加ロジック(addMemo)
- メモ削除ロジック(deleteMemo)
- 新しく追加する:メモ編集ロジック(updateMemo)
- メモ一覧の描画:編集 UI を含めて作る(renderMemos)
- 「メモを追加」ボタンのイベント設定(復習+そのまま)
- 初期処理:保存データを読み込んでから描画
- 2日目の完成コード(JavaScript全体)
- 2日目で本当に掴んでほしいこと
- メモ帳アプリ 3日目のゴール
- 今日の全体ステップ
- 3日目の HTML レイアウト
- 状態と DOM 参照の定義
- localStorage の保存・読み込み(復習)
- メモ追加・編集・削除ロジック(2日目の復習)
- 3日目の心臓部:検索+並び替え付きの描画(renderMemos)
- 検索入力欄のイベント設定
- 並び順ボタンのイベント設定(querySelectorAll+dataset+classList)
- 「メモを追加」ボタンのイベント設定(復習)
- 初期処理:保存データを読み込んでから描画
- 3日目の完成コード(JavaScript全体)
- 3日目で本当に掴んでほしいこと
- メモ帳アプリ 4日目のゴール
- 今日の全体ステップ
- 4日目の HTML レイアウト
- 状態と DOM 参照の定義
- localStorage の保存・読み込み
- 色ラベルの選択値を取得する関数
- メモ追加ロジック:タグと色ラベルを含める
- メモ編集ロジック:タグと色も編集対象にする
- メモ削除ロジック(復習)
- 4日目の心臓部:タグ+色ラベル付きの描画(renderMemos)
- 検索入力欄のイベント設定(復習+タグも対象)
- 並び順ボタンのイベント設定(復習)
- タグフィルタボタンのイベント設定(querySelectorAll+dataset+classList)
- 「メモを追加」ボタンのイベント設定:タグと色を渡す
- 初期処理:保存データを読み込んでから描画
- 4日目の完成コード(JavaScript全体)
- 4日目で本当に掴んでほしいこと
- メモ帳アプリ 5日目のゴール
- 今日の全体ステップ
- 5日目の HTML レイアウト
- 状態と DOM 参照の定義
- localStorage の保存・読み込み
- 色ラベルの選択値を取得する関数(復習)
- メモ追加ロジック:ピン/アーカイブの初期値を持たせる
- メモ編集ロジック:タグ・色はそのまま、ピン/アーカイブは別操作にする
- ピン状態を切り替えるロジック
- アーカイブ状態を切り替えるロジック
- メモ削除ロジック(復習)
- 5日目の心臓部:ピン+アーカイブ+表示モード付きの描画(renderMemos)
- 検索入力欄のイベント設定(復習)
- 並び順ボタンのイベント設定(復習)
- タグフィルタボタンのイベント設定(復習)
- 表示モードフィルタボタンのイベント設定
- 「メモを追加」ボタンのイベント設定
- 初期処理:保存データを読み込んでから描画
- 5日目の完成コード(JavaScript全体)
- 5日目で本当に掴んでほしいこと
- メモ帳アプリ 6日目のゴール
- 今日の全体ステップ
- 6日目の HTML レイアウト
- 状態と DOM 参照の定義
- localStorage の保存・読み込み(メモ本体)
- 下書きの保存・読み込み
- 色ラベルの選択値を取得する関数(復習)
- 文字数カウンターの更新
- メモ追加ロジック:下書きのクリアも行う
- メモ編集・ピン・アーカイブ・削除ロジック(復習+削除確認)
- 全メモ削除ロジック
- 6日目の心臓部:描画(renderMemos)
- エクスポート機能:JSONを生成してテキストエリアに表示
- インポート機能:JSONから復元
- イベント設定まとめ
- 初期処理
- 6日目で本当に掴んでほしいこと
- メモ帳アプリ 7日目のゴール
- 今日の全体ステップ
- アプリの構造をざっくり整理する
- よく出てくる処理を小さな関数に切り出す
- キーボード操作を追加する
- 完成版テンプレート:HTML+JavaScript
- 7日目で本当に掴んでほしいこと
メモ帳アプリ 2日目のゴール
2日目のテーマは 「メモを“読むだけ”から、“編集できる”メモ帳に育てる」 ことです。
1日目で作ったメモ帳アプリは、
- タイトル+本文を入力して追加
- 一覧表示
- 削除
- localStorage に保存
までできる状態でした。
2日目ではここから一歩進めて、
- メモを「編集」できるようにする
- 編集用の UI を DOM で切り替える
datasetで「どのメモか」を特定するclassListで「編集モード」を見た目に反映する
というところまでを、ステップバイステップで作っていきます。
今日の全体ステップ
2日目の流れはこんな感じです。
- 1日目の完成コードをベースにする
- メモ1件分の表示に「編集ボタン」と「編集用フォーム」を追加する
- 「表示モード」と「編集モード」を切り替える処理を書く
- 編集内容を配列に反映し、localStorage に保存する
- 再描画して編集結果を画面に反映する
1日目のベース(HTML)はそのまま使う
HTML は 1日目と同じで構いません。 JavaScript側で「編集機能」を足していきます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>メモ帳アプリ 2日目</title>
<style>
.memo-item {
border: 1px solid #ccc;
padding: 8px;
margin-bottom: 8px;
}
.memo-item-title {
font-weight: bold;
margin-bottom: 4px;
}
.memo-item-body {
white-space: pre-wrap;
}
/* 編集モードのときに背景色を変える例 */
.editing {
background-color: #fff7e6;
}
</style>
</head>
<body>
<h1>メモ帳アプリ(2日目)</h1>
<div>
<label for="memoTitleInput">タイトル:</label>
<input id="memoTitleInput" type="text" placeholder="メモのタイトルを入力してください">
</div>
<div>
<label for="memoBodyInput">本文:</label><br>
<textarea id="memoBodyInput" rows="4" cols="40" placeholder="メモの本文を入力してください"></textarea>
</div>
<div>
<button id="addMemoButton">メモを追加</button>
</div>
<p id="errorMessage" style="color: red;"></p>
<h2>メモ一覧</h2>
<div id="memoList"></div>
<script>
// ここに JavaScript を書いていきます
</script>
</body>
</html>
状態と DOM 参照の定義(1日目と同じ+α)
まずは、アプリの状態と DOM 参照を定義します。
// --- アプリの状態(State) ---
let memos = [];
let nextMemoId = 1;
const STORAGE_KEY = "my_memo_app_data";
// --- DOM参照 ---
const memoTitleInput = document.querySelector("#memoTitleInput");
const memoBodyInput = document.querySelector("#memoBodyInput");
const addMemoButton = document.querySelector("#addMemoButton");
const errorMessage = document.querySelector("#errorMessage");
const memoList = document.querySelector("#memoList");
JavaScriptここまでは 1日目と同じです。 この上に「編集機能」を積み上げていきます。
localStorage の保存・読み込み(復習+そのまま使う)
保存関数
// --- ストレージ:状態をlocalStorageに保存する ---
function saveToStorage() {
const data = {
memos: memos,
nextMemoId: nextMemoId
};
const json = JSON.stringify(data);
localStorage.setItem(STORAGE_KEY, json);
}
JavaScript読み込み関数
// --- ストレージ:localStorageから状態を読み込む ---
function loadFromStorage() {
const json = localStorage.getItem(STORAGE_KEY);
if (json === null) {
return;
}
try {
const data = JSON.parse(json);
if (Array.isArray(data.memos) && typeof data.nextMemoId === "number") {
memos = data.memos;
nextMemoId = data.nextMemoId;
}
} catch (error) {
console.error("保存データの読み込みに失敗しました:", error);
}
}
JavaScriptlocalStorage 周りは 1日目と同じロジックをそのまま使います。 今日は「編集した内容を保存する」場面でこの関数が活躍します。
メモ追加ロジック(addMemo)
追加ロジックも 1日目と同じです。
// --- 操作ロジック:メモを追加する ---
function addMemo(title, body) {
const newMemo = {
id: nextMemoId,
title: title,
body: body,
createdAt: Date.now()
};
nextMemoId = nextMemoId + 1;
memos.push(newMemo);
saveToStorage();
}
JavaScriptメモ削除ロジック(deleteMemo)
削除ロジックも 1日目と同じ形で使えます。
// --- 操作ロジック:メモを削除する ---
function deleteMemo(id) {
const newMemos = memos.filter(function(memo) {
return memo.id !== id;
});
memos.length = 0;
newMemos.forEach(function(memo) {
memos.push(memo);
});
saveToStorage();
renderMemos();
}
JavaScript新しく追加する:メモ編集ロジック(updateMemo)
2日目の主役となる関数です。 指定した ID のメモのタイトルと本文を更新します。
// --- 操作ロジック:メモを編集する ---
function updateMemo(id, newTitle, newBody) {
memos.forEach(function(memo) {
if (memo.id === id) {
memo.title = newTitle;
memo.body = newBody;
}
});
// 編集したら保存
saveToStorage();
// 新しい状態で画面を作り直す
renderMemos();
}
JavaScript深掘りポイント:
- 「どのメモか」を特定するために
idを使う forEachで配列を回しながら、memo.id === idのものだけを書き換える- 編集後は必ず
saveToStorage()→renderMemos()の流れで 画面と保存内容を同期させる
メモ一覧の描画:編集 UI を含めて作る(renderMemos)
ここが 2日目の一番大事な部分です。 1日目の renderMemos() を「編集機能付き」に書き換えます。
// --- 描画:状態から画面(DOM)を作り直す ---
function renderMemos() {
// 一覧をいったん空にする
memoList.innerHTML = "";
// memos配列の1件ごとにDOMを作る
memos.forEach(function(memo) {
// メモ全体を包むdiv
const memoDiv = document.createElement("div");
memoDiv.classList.add("memo-item");
memoDiv.dataset.id = String(memo.id); // このメモのIDを埋め込む
// --- 表示モード用の要素 ---
const titleDiv = document.createElement("div");
titleDiv.classList.add("memo-item-title");
titleDiv.textContent = memo.title;
const bodyDiv = document.createElement("div");
bodyDiv.classList.add("memo-item-body");
bodyDiv.textContent = memo.body;
// --- 編集モード用の要素 ---
const editTitleInput = document.createElement("input");
editTitleInput.type = "text";
editTitleInput.value = memo.title;
editTitleInput.style.display = "none"; // 最初は非表示
const editBodyTextarea = document.createElement("textarea");
editBodyTextarea.rows = 4;
editBodyTextarea.cols = 40;
editBodyTextarea.value = memo.body;
editBodyTextarea.style.display = "none"; // 最初は非表示
// --- ボタン類 ---
const editButton = document.createElement("button");
editButton.textContent = "編集";
const saveButton = document.createElement("button");
saveButton.textContent = "保存";
saveButton.style.display = "none"; // 編集モードでだけ表示
const cancelButton = document.createElement("button");
cancelButton.textContent = "キャンセル";
cancelButton.style.display = "none"; // 編集モードでだけ表示
const deleteButton = document.createElement("button");
deleteButton.textContent = "削除";
// --- 編集ボタンのイベント ---
editButton.addEventListener("click", function() {
// 表示モードの要素を隠す
titleDiv.style.display = "none";
bodyDiv.style.display = "none";
// 編集モードの要素を表示する
editTitleInput.style.display = "block";
editBodyTextarea.style.display = "block";
// ボタンの表示切り替え
editButton.style.display = "none";
deleteButton.style.display = "none";
saveButton.style.display = "inline-block";
cancelButton.style.display = "inline-block";
// 編集中であることを見た目で分かるようにクラスを付ける
memoDiv.classList.add("editing");
// 現在の内容を入力欄にセット(念のため)
editTitleInput.value = memo.title;
editBodyTextarea.value = memo.body;
// タイトル入力欄にフォーカス
editTitleInput.focus();
});
// --- 保存ボタンのイベント ---
saveButton.addEventListener("click", function() {
const newTitle = editTitleInput.value;
const newBody = editBodyTextarea.value;
// タイトルと本文の両方が空ならエラー
if (newTitle.trim() === "" && newBody.trim() === "") {
alert("タイトルか本文のどちらかは入力してください。");
return;
}
// IDを取り出す(datasetから)
const idText = memoDiv.dataset.id;
const id = Number(idText);
// 状態を更新して再描画
updateMemo(id, newTitle, newBody);
});
// --- キャンセルボタンのイベント ---
cancelButton.addEventListener("click", function() {
// 編集モードをやめて、元の表示に戻す
// 表示モードの要素を表示
titleDiv.style.display = "block";
bodyDiv.style.display = "block";
// 編集モードの要素を隠す
editTitleInput.style.display = "none";
editBodyTextarea.style.display = "none";
// ボタンの表示切り替え
editButton.style.display = "inline-block";
deleteButton.style.display = "inline-block";
saveButton.style.display = "none";
cancelButton.style.display = "none";
// 編集中クラスを外す
memoDiv.classList.remove("editing");
});
// --- 削除ボタンのイベント ---
deleteButton.addEventListener("click", function(event) {
const memoElement = event.target.closest(".memo-item");
const idText = memoElement.dataset.id;
const id = Number(idText);
deleteMemo(id);
});
// --- memoDivの中に要素を追加する ---
// 表示モード用
memoDiv.append(titleDiv);
memoDiv.append(bodyDiv);
// 編集モード用
memoDiv.append(editTitleInput);
memoDiv.append(editBodyTextarea);
// ボタン類
memoDiv.append(editButton);
memoDiv.append(saveButton);
memoDiv.append(cancelButton);
memoDiv.append(deleteButton);
// memoListの中にmemoDivを追加する
memoList.append(memoDiv);
});
}
JavaScriptこの描画関数で押さえてほしいポイント
- 表示モードと編集モードを「別のDOM要素」として用意しておき、
style.displayで切り替える classList.add("editing")/classList.remove("editing")で、 編集中かどうかを見た目に反映できるdataset.idに ID を埋め込んでおくことで、 ボタンから「どのメモか」を簡単に特定できる- 編集後は
updateMemo()→saveToStorage()→renderMemos()の流れで 状態と画面を同期させる
DOM 操作・イベント・配列・localStorage が 1つの関数の中で自然につながっているのが分かると思います。
「メモを追加」ボタンのイベント設定(復習+そのまま)
追加ボタンのイベントは 1日目と同じです。
// --- イベント設定:メモを追加ボタン ---
addMemoButton.addEventListener("click", function() {
const title = memoTitleInput.value;
const body = memoBodyInput.value;
errorMessage.textContent = "";
if (title.trim() === "" && body.trim() === "") {
errorMessage.textContent = "タイトルか本文のどちらかは入力してください。";
return;
}
addMemo(title, body);
memoTitleInput.value = "";
memoBodyInput.value = "";
renderMemos();
});
JavaScript初期処理:保存データを読み込んでから描画
最後に、ページ読み込み時の流れを整えます。
// --- 初期処理 ---
loadFromStorage();
renderMemos();
JavaScript2日目の完成コード(JavaScript全体)
HTML の <script> タグの中身として、 ここまでのコードをひとつにまとめるとこうなります。
<script>
// --- アプリの状態(State) ---
let memos = [];
let nextMemoId = 1;
const STORAGE_KEY = "my_memo_app_data";
// --- DOM参照 ---
const memoTitleInput = document.querySelector("#memoTitleInput");
const memoBodyInput = document.querySelector("#memoBodyInput");
const addMemoButton = document.querySelector("#addMemoButton");
const errorMessage = document.querySelector("#errorMessage");
const memoList = document.querySelector("#memoList");
// --- ストレージ:保存 ---
function saveToStorage() {
const data = {
memos: memos,
nextMemoId: nextMemoId
};
const json = JSON.stringify(data);
localStorage.setItem(STORAGE_KEY, json);
}
// --- ストレージ:読み込み ---
function loadFromStorage() {
const json = localStorage.getItem(STORAGE_KEY);
if (json === null) {
return;
}
try {
const data = JSON.parse(json);
if (Array.isArray(data.memos) && typeof data.nextMemoId === "number") {
memos = data.memos;
nextMemoId = data.nextMemoId;
}
} catch (error) {
console.error("保存データの読み込みに失敗しました:", error);
}
}
// --- 操作ロジック:メモ追加 ---
function addMemo(title, body) {
const newMemo = {
id: nextMemoId,
title: title,
body: body,
createdAt: Date.now()
};
nextMemoId = nextMemoId + 1;
memos.push(newMemo);
saveToStorage();
}
// --- 操作ロジック:メモ編集 ---
function updateMemo(id, newTitle, newBody) {
memos.forEach(function(memo) {
if (memo.id === id) {
memo.title = newTitle;
memo.body = newBody;
}
});
saveToStorage();
renderMemos();
}
// --- 操作ロジック:メモ削除 ---
function deleteMemo(id) {
const newMemos = memos.filter(function(memo) {
return memo.id !== id;
});
memos.length = 0;
newMemos.forEach(function(memo) {
memos.push(memo);
});
saveToStorage();
renderMemos();
}
// --- 描画 ---
function renderMemos() {
memoList.innerHTML = "";
memos.forEach(function(memo) {
const memoDiv = document.createElement("div");
memoDiv.classList.add("memo-item");
memoDiv.dataset.id = String(memo.id);
const titleDiv = document.createElement("div");
titleDiv.classList.add("memo-item-title");
titleDiv.textContent = memo.title;
const bodyDiv = document.createElement("div");
bodyDiv.classList.add("memo-item-body");
bodyDiv.textContent = memo.body;
const editTitleInput = document.createElement("input");
editTitleInput.type = "text";
editTitleInput.value = memo.title;
editTitleInput.style.display = "none";
const editBodyTextarea = document.createElement("textarea");
editBodyTextarea.rows = 4;
editBodyTextarea.cols = 40;
editBodyTextarea.value = memo.body;
editBodyTextarea.style.display = "none";
const editButton = document.createElement("button");
editButton.textContent = "編集";
const saveButton = document.createElement("button");
saveButton.textContent = "保存";
saveButton.style.display = "none";
const cancelButton = document.createElement("button");
cancelButton.textContent = "キャンセル";
cancelButton.style.display = "none";
const deleteButton = document.createElement("button");
deleteButton.textContent = "削除";
editButton.addEventListener("click", function() {
titleDiv.style.display = "none";
bodyDiv.style.display = "none";
editTitleInput.style.display = "block";
editBodyTextarea.style.display = "block";
editButton.style.display = "none";
deleteButton.style.display = "none";
saveButton.style.display = "inline-block";
cancelButton.style.display = "inline-block";
memoDiv.classList.add("editing");
editTitleInput.value = memo.title;
editBodyTextarea.value = memo.body;
editTitleInput.focus();
});
saveButton.addEventListener("click", function() {
const newTitle = editTitleInput.value;
const newBody = editBodyTextarea.value;
if (newTitle.trim() === "" && newBody.trim() === "") {
alert("タイトルか本文のどちらかは入力してください。");
return;
}
const idText = memoDiv.dataset.id;
const id = Number(idText);
updateMemo(id, newTitle, newBody);
});
cancelButton.addEventListener("click", function() {
titleDiv.style.display = "block";
bodyDiv.style.display = "block";
editTitleInput.style.display = "none";
editBodyTextarea.style.display = "none";
editButton.style.display = "inline-block";
deleteButton.style.display = "inline-block";
saveButton.style.display = "none";
cancelButton.style.display = "none";
memoDiv.classList.remove("editing");
});
deleteButton.addEventListener("click", function(event) {
const memoElement = event.target.closest(".memo-item");
const idText = memoElement.dataset.id;
const id = Number(idText);
deleteMemo(id);
});
memoDiv.append(titleDiv);
memoDiv.append(bodyDiv);
memoDiv.append(editTitleInput);
memoDiv.append(editBodyTextarea);
memoDiv.append(editButton);
memoDiv.append(saveButton);
memoDiv.append(cancelButton);
memoDiv.append(deleteButton);
memoList.append(memoDiv);
});
}
// --- イベント設定:メモ追加ボタン ---
addMemoButton.addEventListener("click", function() {
const title = memoTitleInput.value;
const body = memoBodyInput.value;
errorMessage.textContent = "";
if (title.trim() === "" && body.trim() === "") {
errorMessage.textContent = "タイトルか本文のどちらかは入力してください。";
return;
}
addMemo(title, body);
memoTitleInput.value = "";
memoBodyInput.value = "";
renderMemos();
});
// --- 初期処理 ---
loadFromStorage();
renderMemos();
</script>
2日目で本当に掴んでほしいこと
- DOM要素を「表示用」と「編集用」に分けて、
style.displayとclassListで切り替えると、UIの状態管理がぐっと楽になる datasetに ID を埋め込んでおくことで、ボタンから「どのデータか」を簡単に特定できる- 配列(状態)→ localStorage(保存)→ DOM(描画)→ イベント(操作)という流れが、メモ帳アプリでも自然に回り始めている
この構造を一度掴んでしまえば、 ToDoアプリでも、メモ帳でも、家計簿でも、 「編集できる UI」を自分で設計していけるようになります。
メモ帳アプリ 3日目のゴール
3日目のテーマは 「メモが増えても“探せる・整理できる”メモ帳にする」 ことです。
1〜2日目で作ったメモ帳アプリは、
- メモの追加
- 編集
- 削除
- localStorage に保存
までできる状態でした。
3日目ではここから一歩進めて、
- キーワードでメモを「検索」できる
- 並び順(新しい順/古い順)を切り替えられる
querySelectorAll()+dataset+classListを使って 「複数ボタンで状態を切り替える」パターンを身につける
というところまでを、ステップバイステップで作っていきます。
今日の全体ステップ
3日目の流れはこんな感じです。
- 検索用の入力欄と並び順ボタンを HTML に追加する
- JavaScript で検索キーワードと並び順モードを状態として持つ
renderMemos()の中で- キーワードでフィルタ
- 並び順モードでソート を行う
- 検索入力欄の
inputイベントで再描画する - 並び順ボタンを
querySelectorAll()でまとめて取得し、datasetとclassListで切り替える
3日目の HTML レイアウト
2日目までの HTML に、 「検索」と「並び順」の UI を追加します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>メモ帳アプリ 3日目</title>
<style>
.memo-item {
border: 1px solid #ccc;
padding: 8px;
margin-bottom: 8px;
}
.memo-item-title {
font-weight: bold;
margin-bottom: 4px;
}
.memo-item-body {
white-space: pre-wrap;
}
.editing {
background-color: #fff7e6;
}
/* 並び順ボタンの選択状態を見た目で分かるようにする */
.active-sort {
background-color: #007bff;
color: white;
}
</style>
</head>
<body>
<h1>メモ帳アプリ(3日目)</h1>
<div>
<label for="memoTitleInput">タイトル:</label>
<input id="memoTitleInput" type="text" placeholder="メモのタイトルを入力してください">
</div>
<div>
<label for="memoBodyInput">本文:</label><br>
<textarea id="memoBodyInput" rows="4" cols="40" placeholder="メモの本文を入力してください"></textarea>
</div>
<div>
<button id="addMemoButton">メモを追加</button>
</div>
<p id="errorMessage" style="color: red;"></p>
<h2>検索</h2>
<div>
<input id="searchInput" type="text" placeholder="タイトル・本文から検索">
</div>
<h2>並び順</h2>
<div id="sortButtons">
<button data-sort="newest" class="active-sort">新しい順</button>
<button data-sort="oldest">古い順</button>
</div>
<h2>メモ一覧</h2>
<div id="memoList"></div>
<script>
// ここに JavaScript を書いていきます
</script>
</body>
</html>
ここで押さえておきたいポイント
- 検索用に
#searchInputを追加 - 並び順ボタンを
#sortButtonsの中に2つ配置data-sort="newest"data-sort="oldest"
- 「今選ばれている並び順」を見た目で分かるように
.active-sortクラスを用意
この「data属性+クラス+複数ボタン」の組み合わせが、 3日目の重要な練習ポイントです。
状態と DOM 参照の定義
3日目では、 「検索キーワード」と「並び順モード」を状態として追加します。
// --- アプリの状態(State) ---
let memos = [];
let nextMemoId = 1;
// 検索キーワード(空文字なら全件表示)
let currentKeyword = "";
// 並び順モード("newest" または "oldest")
let currentSort = "newest";
const STORAGE_KEY = "my_memo_app_data";
// --- DOM参照 ---
const memoTitleInput = document.querySelector("#memoTitleInput");
const memoBodyInput = document.querySelector("#memoBodyInput");
const addMemoButton = document.querySelector("#addMemoButton");
const errorMessage = document.querySelector("#errorMessage");
const memoList = document.querySelector("#memoList");
// 検索入力欄
const searchInput = document.querySelector("#searchInput");
// 並び順ボタン(複数)をまとめて取得
const sortButtons = document.querySelectorAll("#sortButtons button");
JavaScript深掘りポイント:
- 「検索キーワード」や「並び順モード」も、 立派な「アプリの状態」です
querySelectorAll()で複数ボタンをまとめて取得しておくと、forEach()で一括処理できる
localStorage の保存・読み込み(復習)
検索キーワードや並び順は「一時的な状態」として扱い、 localStorage には保存しません。 保存するのはメモ一覧と次のIDだけです。
// --- ストレージ:状態をlocalStorageに保存する ---
function saveToStorage() {
const data = {
memos: memos,
nextMemoId: nextMemoId
};
const json = JSON.stringify(data);
localStorage.setItem(STORAGE_KEY, json);
}
// --- ストレージ:localStorageから状態を読み込む ---
function loadFromStorage() {
const json = localStorage.getItem(STORAGE_KEY);
if (json === null) {
return;
}
try {
const data = JSON.parse(json);
if (Array.isArray(data.memos) && typeof data.nextMemoId === "number") {
memos = data.memos;
nextMemoId = data.nextMemoId;
}
} catch (error) {
console.error("保存データの読み込みに失敗しました:", error);
}
}
JavaScriptメモ追加・編集・削除ロジック(2日目の復習)
メモ追加
// --- 操作ロジック:メモを追加する ---
function addMemo(title, body) {
const newMemo = {
id: nextMemoId,
title: title,
body: body,
createdAt: Date.now()
};
nextMemoId = nextMemoId + 1;
memos.push(newMemo);
saveToStorage();
}
JavaScriptメモ編集
// --- 操作ロジック:メモを編集する ---
function updateMemo(id, newTitle, newBody) {
memos.forEach(function(memo) {
if (memo.id === id) {
memo.title = newTitle;
memo.body = newBody;
}
});
saveToStorage();
renderMemos();
}
JavaScriptメモ削除
// --- 操作ロジック:メモを削除する ---
function deleteMemo(id) {
const newMemos = memos.filter(function(memo) {
return memo.id !== id;
});
memos.length = 0;
newMemos.forEach(function(memo) {
memos.push(memo);
});
saveToStorage();
renderMemos();
}
JavaScript3日目の心臓部:検索+並び替え付きの描画(renderMemos)
ここが今日の一番大事な部分です。 renderMemos() の中で、
- 検索キーワードでフィルタ
- 並び順モードでソート
- DOM を生成
という流れを作ります。
// --- 描画:状態から画面(DOM)を作り直す ---
function renderMemos() {
// 一覧をいったん空にする
memoList.innerHTML = "";
// 1. 検索キーワードでフィルタする
let visibleMemos = memos;
if (currentKeyword.trim() !== "") {
const keywordLower = currentKeyword.toLowerCase();
visibleMemos = memos.filter(function(memo) {
// タイトルと本文を小文字にしてから検索
const titleLower = memo.title.toLowerCase();
const bodyLower = memo.body.toLowerCase();
// キーワードがタイトルか本文に含まれているか
return (
titleLower.includes(keywordLower) ||
bodyLower.includes(keywordLower)
);
});
}
// 2. 並び替えのためにコピーを作る(元の配列を壊さない)
visibleMemos = visibleMemos.slice();
// 3. 並び順モードに応じてソートする
if (currentSort === "newest") {
// 新しい順(createdAtが大きいものを前に)
visibleMemos.sort(function(a, b) {
return b.createdAt - a.createdAt;
});
} else if (currentSort === "oldest") {
// 古い順(createdAtが小さいものを前に)
visibleMemos.sort(function(a, b) {
return a.createdAt - b.createdAt;
});
}
// 4. ソート済みのvisibleMemosをもとにDOMを作る
visibleMemos.forEach(function(memo) {
const memoDiv = document.createElement("div");
memoDiv.classList.add("memo-item");
memoDiv.dataset.id = String(memo.id);
// --- 表示モード用の要素 ---
const titleDiv = document.createElement("div");
titleDiv.classList.add("memo-item-title");
titleDiv.textContent = memo.title;
const bodyDiv = document.createElement("div");
bodyDiv.classList.add("memo-item-body");
bodyDiv.textContent = memo.body;
// --- 編集モード用の要素 ---
const editTitleInput = document.createElement("input");
editTitleInput.type = "text";
editTitleInput.value = memo.title;
editTitleInput.style.display = "none";
const editBodyTextarea = document.createElement("textarea");
editBodyTextarea.rows = 4;
editBodyTextarea.cols = 40;
editBodyTextarea.value = memo.body;
editBodyTextarea.style.display = "none";
// --- ボタン類 ---
const editButton = document.createElement("button");
editButton.textContent = "編集";
const saveButton = document.createElement("button");
saveButton.textContent = "保存";
saveButton.style.display = "none";
const cancelButton = document.createElement("button");
cancelButton.textContent = "キャンセル";
cancelButton.style.display = "none";
const deleteButton = document.createElement("button");
deleteButton.textContent = "削除";
// --- 編集ボタンのイベント ---
editButton.addEventListener("click", function() {
titleDiv.style.display = "none";
bodyDiv.style.display = "none";
editTitleInput.style.display = "block";
editBodyTextarea.style.display = "block";
editButton.style.display = "none";
deleteButton.style.display = "none";
saveButton.style.display = "inline-block";
cancelButton.style.display = "inline-block";
memoDiv.classList.add("editing");
editTitleInput.value = memo.title;
editBodyTextarea.value = memo.body;
editTitleInput.focus();
});
// --- 保存ボタンのイベント ---
saveButton.addEventListener("click", function() {
const newTitle = editTitleInput.value;
const newBody = editBodyTextarea.value;
if (newTitle.trim() === "" && newBody.trim() === "") {
alert("タイトルか本文のどちらかは入力してください。");
return;
}
const idText = memoDiv.dataset.id;
const id = Number(idText);
updateMemo(id, newTitle, newBody);
});
// --- キャンセルボタンのイベント ---
cancelButton.addEventListener("click", function() {
titleDiv.style.display = "block";
bodyDiv.style.display = "block";
editTitleInput.style.display = "none";
editBodyTextarea.style.display = "none";
editButton.style.display = "inline-block";
deleteButton.style.display = "inline-block";
saveButton.style.display = "none";
cancelButton.style.display = "none";
memoDiv.classList.remove("editing");
});
// --- 削除ボタンのイベント ---
deleteButton.addEventListener("click", function(event) {
const memoElement = event.target.closest(".memo-item");
const idText = memoElement.dataset.id;
const id = Number(idText);
deleteMemo(id);
});
// --- memoDivの中に要素を追加する ---
memoDiv.append(titleDiv);
memoDiv.append(bodyDiv);
memoDiv.append(editTitleInput);
memoDiv.append(editBodyTextarea);
memoDiv.append(editButton);
memoDiv.append(saveButton);
memoDiv.append(cancelButton);
memoDiv.append(deleteButton);
memoList.append(memoDiv);
});
}
JavaScriptこの関数で本当に掴んでほしいこと
- 「状態 → フィルタ → コピー → ソート → DOM生成」という流れが、 検索・並び替え付きのアプリの基本パターンになる
- 検索キーワードは
toLowerCase()で小文字に揃えてからincludes()で判定すると、 大文字・小文字の違いを気にせず検索できる - 並び順は「状態(currentSort)」と「ボタンの見た目(classList)」を きちんと分けて考えると整理しやすい
検索入力欄のイベント設定
検索欄に文字が入力されるたびに、 currentKeyword を更新して renderMemos() を呼びます。
// --- イベント設定:検索入力欄 ---
searchInput.addEventListener("input", function() {
// 入力されたキーワードを状態に保存
currentKeyword = searchInput.value;
// キーワードが変わったので、表示を更新
renderMemos();
});
JavaScriptポイント:
inputイベントは「文字が入力されるたび」に発火する- 検索は「状態を変えて再描画する」だけで実現できる
並び順ボタンのイベント設定(querySelectorAll+dataset+classList)
ここで querySelectorAll() と dataset と classList を フル活用します。
// --- イベント設定:並び順ボタン ---
sortButtons.forEach(function(button) {
button.addEventListener("click", function() {
// ボタンに埋め込んでおいた並び順モードを取り出す
const sortMode = button.dataset.sort; // "newest" または "oldest"
// 状態を更新
currentSort = sortMode;
// 全ての並び順ボタンから「選択中クラス」を外す
sortButtons.forEach(function(b) {
b.classList.remove("active-sort");
});
// クリックされたボタンだけ「選択中クラス」を付ける
button.classList.add("active-sort");
// 新しい並び順で画面を作り直す
renderMemos();
});
});
JavaScriptここで押さえてほしいこと
querySelectorAll()で複数ボタンをまとめて取得し、forEach()で一括処理するdataset.sortに「このボタンが表す意味(newest / oldest)」を埋め込んでおくと、 イベントの中で簡単に取り出せるclassList.remove()/classList.add()で「選択中のボタン」を見た目で表現する
このパターンは、 タブ切り替え・フィルタボタン・モード切り替えなど、 いろいろな UI で使える強力な型です。
「メモを追加」ボタンのイベント設定(復習)
追加ボタンは 2日目と同じです。
// --- イベント設定:メモを追加ボタン ---
addMemoButton.addEventListener("click", function() {
const title = memoTitleInput.value;
const body = memoBodyInput.value;
errorMessage.textContent = "";
if (title.trim() === "" && body.trim() === "") {
errorMessage.textContent = "タイトルか本文のどちらかは入力してください。";
return;
}
addMemo(title, body);
memoTitleInput.value = "";
memoBodyInput.value = "";
renderMemos();
});
JavaScript初期処理:保存データを読み込んでから描画
最後に、ページ読み込み時の流れを整えます。
// --- 初期処理 ---
loadFromStorage();
renderMemos();
JavaScript3日目の完成コード(JavaScript全体)
HTML の <script> タグの中身として、 ここまでのコードをひとつにまとめるとこうなります。
<script>
// --- アプリの状態(State) ---
let memos = [];
let nextMemoId = 1;
let currentKeyword = "";
let currentSort = "newest";
const STORAGE_KEY = "my_memo_app_data";
// --- DOM参照 ---
const memoTitleInput = document.querySelector("#memoTitleInput");
const memoBodyInput = document.querySelector("#memoBodyInput");
const addMemoButton = document.querySelector("#addMemoButton");
const errorMessage = document.querySelector("#errorMessage");
const memoList = document.querySelector("#memoList");
const searchInput = document.querySelector("#searchInput");
const sortButtons = document.querySelectorAll("#sortButtons button");
// --- ストレージ:保存 ---
function saveToStorage() {
const data = {
memos: memos,
nextMemoId: nextMemoId
};
const json = JSON.stringify(data);
localStorage.setItem(STORAGE_KEY, json);
}
// --- ストレージ:読み込み ---
function loadFromStorage() {
const json = localStorage.getItem(STORAGE_KEY);
if (json === null) {
return;
}
try {
const data = JSON.parse(json);
if (Array.isArray(data.memos) && typeof data.nextMemoId === "number") {
memos = data.memos;
nextMemoId = data.nextMemoId;
}
} catch (error) {
console.error("保存データの読み込みに失敗しました:", error);
}
}
// --- 操作ロジック:メモ追加 ---
function addMemo(title, body) {
const newMemo = {
id: nextMemoId,
title: title,
body: body,
createdAt: Date.now()
};
nextMemoId = nextMemoId + 1;
memos.push(newMemo);
saveToStorage();
}
// --- 操作ロジック:メモ編集 ---
function updateMemo(id, newTitle, newBody) {
memos.forEach(function(memo) {
if (memo.id === id) {
memo.title = newTitle;
memo.body = newBody;
}
});
saveToStorage();
renderMemos();
}
// --- 操作ロジック:メモ削除 ---
function deleteMemo(id) {
const newMemos = memos.filter(function(memo) {
return memo.id !== id;
});
memos.length = 0;
newMemos.forEach(function(memo) {
memos.push(memo);
});
saveToStorage();
renderMemos();
}
// --- 描画 ---
function renderMemos() {
memoList.innerHTML = "";
let visibleMemos = memos;
if (currentKeyword.trim() !== "") {
const keywordLower = currentKeyword.toLowerCase();
visibleMemos = memos.filter(function(memo) {
const titleLower = memo.title.toLowerCase();
const bodyLower = memo.body.toLowerCase();
return (
titleLower.includes(keywordLower) ||
bodyLower.includes(keywordLower)
);
});
}
visibleMemos = visibleMemos.slice();
if (currentSort === "newest") {
visibleMemos.sort(function(a, b) {
return b.createdAt - a.createdAt;
});
} else if (currentSort === "oldest") {
visibleMemos.sort(function(a, b) {
return a.createdAt - b.createdAt;
});
}
visibleMemos.forEach(function(memo) {
const memoDiv = document.createElement("div");
memoDiv.classList.add("memo-item");
memoDiv.dataset.id = String(memo.id);
const titleDiv = document.createElement("div");
titleDiv.classList.add("memo-item-title");
titleDiv.textContent = memo.title;
const bodyDiv = document.createElement("div");
bodyDiv.classList.add("memo-item-body");
bodyDiv.textContent = memo.body;
const editTitleInput = document.createElement("input");
editTitleInput.type = "text";
editTitleInput.value = memo.title;
editTitleInput.style.display = "none";
const editBodyTextarea = document.createElement("textarea");
editBodyTextarea.rows = 4;
editBodyTextarea.cols = 40;
editBodyTextarea.value = memo.body;
editBodyTextarea.style.display = "none";
const editButton = document.createElement("button");
editButton.textContent = "編集";
const saveButton = document.createElement("button");
saveButton.textContent = "保存";
saveButton.style.display = "none";
const cancelButton = document.createElement("button");
cancelButton.textContent = "キャンセル";
cancelButton.style.display = "none";
const deleteButton = document.createElement("button");
deleteButton.textContent = "削除";
editButton.addEventListener("click", function() {
titleDiv.style.display = "none";
bodyDiv.style.display = "none";
editTitleInput.style.display = "block";
editBodyTextarea.style.display = "block";
editButton.style.display = "none";
deleteButton.style.display = "none";
saveButton.style.display = "inline-block";
cancelButton.style.display = "inline-block";
memoDiv.classList.add("editing");
editTitleInput.value = memo.title;
editBodyTextarea.value = memo.body;
editTitleInput.focus();
});
saveButton.addEventListener("click", function() {
const newTitle = editTitleInput.value;
const newBody = editBodyTextarea.value;
if (newTitle.trim() === "" && newBody.trim() === "") {
alert("タイトルか本文のどちらかは入力してください。");
return;
}
const idText = memoDiv.dataset.id;
const id = Number(idText);
updateMemo(id, newTitle, newBody);
});
cancelButton.addEventListener("click", function() {
titleDiv.style.display = "block";
bodyDiv.style.display = "block";
editTitleInput.style.display = "none";
editBodyTextarea.style.display = "none";
editButton.style.display = "inline-block";
deleteButton.style.display = "inline-block";
saveButton.style.display = "none";
cancelButton.style.display = "none";
memoDiv.classList.remove("editing");
});
deleteButton.addEventListener("click", function(event) {
const memoElement = event.target.closest(".memo-item");
const idText = memoElement.dataset.id;
const id = Number(idText);
deleteMemo(id);
});
memoDiv.append(titleDiv);
memoDiv.append(bodyDiv);
memoDiv.append(editTitleInput);
memoDiv.append(editBodyTextarea);
memoDiv.append(editButton);
memoDiv.append(saveButton);
memoDiv.append(cancelButton);
memoDiv.append(deleteButton);
memoList.append(memoDiv);
});
}
// --- イベント設定:メモ追加ボタン ---
addMemoButton.addEventListener("click", function() {
const title = memoTitleInput.value;
const body = memoBodyInput.value;
errorMessage.textContent = "";
if (title.trim() === "" && body.trim() === "") {
errorMessage.textContent = "タイトルか本文のどちらかは入力してください。";
return;
}
addMemo(title, body);
memoTitleInput.value = "";
memoBodyInput.value = "";
renderMemos();
});
// --- イベント設定:検索入力欄 ---
searchInput.addEventListener("input", function() {
currentKeyword = searchInput.value;
renderMemos();
});
// --- イベント設定:並び順ボタン ---
sortButtons.forEach(function(button) {
button.addEventListener("click", function() {
const sortMode = button.dataset.sort;
currentSort = sortMode;
sortButtons.forEach(function(b) {
b.classList.remove("active-sort");
});
button.classList.add("active-sort");
renderMemos();
});
});
// --- 初期処理 ---
loadFromStorage();
renderMemos();
</script>
3日目で本当に掴んでほしいこと
- 検索・並び替えは「状態を変えて再描画する」だけで実現できる
querySelectorAll()+dataset+classListの組み合わせは、 複数ボタンでモードを切り替える UI の基本パターンになる- 配列(状態)→ localStorage(保存)→ DOM(描画)→ イベント(操作)という流れが、 メモ帳アプリの中で自然に回り始めている
この構造を一度体に入れてしまえば、 ToDoアプリでも、メモ帳でも、家計簿でも、 「検索・並び替え付きのアプリ」を自分で設計していけるようになります。
メモ帳アプリ 4日目のゴール
4日目のテーマは 「メモに“意味”を持たせて、タグや色で整理できるメモ帳にする」 ことです。
1〜3日目で作ったメモ帳アプリは、
- 追加・編集・削除
- 検索
- 並び替え(新しい順/古い順)
- localStorage に保存
までできる状態でした。
4日目ではここから一歩進めて、
- メモに「タグ」と「色ラベル」を付けられる
- タグで絞り込み表示ができる
- 色ラベルを見た目に反映する
querySelectorAll()+dataset+classListを 「タグフィルタボタン」にも応用する
というところまでを、ステップバイステップで作っていきます。
今日の全体ステップ
4日目の流れはこんな感じです。
- メモ追加フォームに「タグ」と「色ラベル」を追加する
- メモオブジェクトに
tagとcolorプロパティを追加する - メモ一覧の表示にタグと色ラベルを反映する
- タグフィルタボタンを HTML に追加し、
querySelectorAll()で取得する - タグフィルタの状態を持ち、
renderMemos()の中でフィルタする classListとdatasetで「選択中のタグ」を見た目に表現する
4日目の HTML レイアウト
3日目までの HTML に、 「タグ入力」と「色ラベル選択」と「タグフィルタ」を追加します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>メモ帳アプリ 4日目</title>
<style>
.memo-item {
border: 1px solid #ccc;
padding: 8px;
margin-bottom: 8px;
}
.memo-item-title {
font-weight: bold;
margin-bottom: 4px;
}
.memo-item-body {
white-space: pre-wrap;
}
.memo-meta {
font-size: 12px;
color: #666;
margin-top: 4px;
}
.editing {
background-color: #fff7e6;
}
/* 並び順ボタンの選択状態 */
.active-sort {
background-color: #007bff;
color: white;
}
/* タグフィルタボタンの選択状態 */
.active-tag {
background-color: #28a745;
color: white;
}
/* 色ラベル用のクラス */
.color-red {
border-left: 4px solid #ff4d4f;
}
.color-blue {
border-left: 4px solid #1890ff;
}
.color-yellow {
border-left: 4px solid #faad14;
}
.color-none {
border-left: 4px solid transparent;
}
</style>
</head>
<body>
<h1>メモ帳アプリ(4日目)</h1>
<div>
<label for="memoTitleInput">タイトル:</label>
<input id="memoTitleInput" type="text" placeholder="メモのタイトルを入力してください">
</div>
<div>
<label for="memoBodyInput">本文:</label><br>
<textarea id="memoBodyInput" rows="4" cols="40" placeholder="メモの本文を入力してください"></textarea>
</div>
<div>
<label for="memoTagInput">タグ:</label>
<input id="memoTagInput" type="text" placeholder="例:仕事 / プライベート / アイデア">
</div>
<div>
<span>色ラベル:</span>
<label>
<input type="radio" name="memoColor" value="none" checked> なし
</label>
<label>
<input type="radio" name="memoColor" value="red"> 赤
</label>
<label>
<input type="radio" name="memoColor" value="blue"> 青
</label>
<label>
<input type="radio" name="memoColor" value="yellow"> 黄
</label>
</div>
<div>
<button id="addMemoButton">メモを追加</button>
</div>
<p id="errorMessage" style="color: red;"></p>
<h2>検索</h2>
<div>
<input id="searchInput" type="text" placeholder="タイトル・本文・タグから検索">
</div>
<h2>並び順</h2>
<div id="sortButtons">
<button data-sort="newest" class="active-sort">新しい順</button>
<button data-sort="oldest">古い順</button>
</div>
<h2>タグフィルタ</h2>
<div id="tagFilterButtons">
<button data-tag="" class="active-tag">すべて</button>
<button data-tag="仕事">仕事</button>
<button data-tag="プライベート">プライベート</button>
<button data-tag="アイデア">アイデア</button>
</div>
<h2>メモ一覧</h2>
<div id="memoList"></div>
<script>
// ここに JavaScript を書いていきます
</script>
</body>
</html>
ここで押さえておきたいポイント
- メモ追加フォームに
#memoTagInputと「色ラベル用のラジオボタン」を追加 - タグフィルタ用に
#tagFilterButtonsを用意し、 各ボタンにdata-tag属性でタグ名を埋め込む - 「すべて」ボタンは
data-tag=""として、 「タグフィルタなし」を表現する
タグフィルタボタンは、 3日目の「並び順ボタン」とほぼ同じパターンで扱います。
状態と DOM 参照の定義
4日目では、 「タグフィルタモード」と「色ラベル」を状態として扱います。
// --- アプリの状態(State) ---
let memos = [];
let nextMemoId = 1;
// 検索キーワード
let currentKeyword = "";
// 並び順モード("newest" / "oldest")
let currentSort = "newest";
// タグフィルタ(空文字なら「すべて」)
let currentTagFilter = "";
const STORAGE_KEY = "my_memo_app_data";
// --- DOM参照 ---
const memoTitleInput = document.querySelector("#memoTitleInput");
const memoBodyInput = document.querySelector("#memoBodyInput");
const memoTagInput = document.querySelector("#memoTagInput");
const addMemoButton = document.querySelector("#addMemoButton");
const errorMessage = document.querySelector("#errorMessage");
const memoList = document.querySelector("#memoList");
const searchInput = document.querySelector("#searchInput");
const sortButtons = document.querySelectorAll("#sortButtons button");
const tagFilterButtons = document.querySelectorAll("#tagFilterButtons button");
// 色ラベル用のラジオボタン(複数)
const memoColorRadios = document.querySelectorAll("input[name='memoColor']");
JavaScript深掘りポイント:
- 「タグフィルタ」も立派な状態
- 色ラベルは「メモごとのプロパティ」として保存する
querySelectorAll()で複数ボタン・複数ラジオをまとめて扱う
localStorage の保存・読み込み
メモオブジェクトに tag と color を追加するので、 保存・読み込みはそのままでも動きます(JSONがオブジェクトをそのまま保持してくれるため)。
// --- ストレージ:状態をlocalStorageに保存する ---
function saveToStorage() {
const data = {
memos: memos,
nextMemoId: nextMemoId
};
const json = JSON.stringify(data);
localStorage.setItem(STORAGE_KEY, json);
}
// --- ストレージ:localStorageから状態を読み込む ---
function loadFromStorage() {
const json = localStorage.getItem(STORAGE_KEY);
if (json === null) {
return;
}
try {
const data = JSON.parse(json);
if (Array.isArray(data.memos) && typeof data.nextMemoId === "number") {
memos = data.memos;
nextMemoId = data.nextMemoId;
}
} catch (error) {
console.error("保存データの読み込みに失敗しました:", error);
}
}
JavaScript色ラベルの選択値を取得する関数
ラジオボタンから「選ばれている色ラベル」を取り出す小さな関数を作ります。
// --- ユーティリティ:選択されている色ラベルを取得する ---
function getSelectedColor() {
let selectedValue = "none";
memoColorRadios.forEach(function(radio) {
if (radio.checked) {
selectedValue = radio.value; // "none" / "red" / "blue" / "yellow"
}
});
return selectedValue;
}
JavaScriptポイント:
- ラジオボタンは「同じ name を持つ複数の input」の中で どれか1つだけが
checkedになる querySelectorAll()+forEach()で「どれが選ばれているか」を調べる
メモ追加ロジック:タグと色ラベルを含める
メモオブジェクトに tag と color を追加します。
// --- 操作ロジック:メモを追加する ---
function addMemo(title, body, tag, color) {
const newMemo = {
id: nextMemoId,
title: title,
body: body,
tag: tag, // タグ(文字列)
color: color, // 色ラベル("none" / "red" / "blue" / "yellow")
createdAt: Date.now()
};
nextMemoId = nextMemoId + 1;
memos.push(newMemo);
saveToStorage();
}
JavaScript深掘りポイント:
- メモの「意味」を持たせるために、 タグや色ラベルをオブジェクトのプロパティとして持たせる
- 後でフィルタや見た目に使えるように、 シンプルな文字列で持っておく
メモ編集ロジック:タグと色も編集対象にする
編集時にもタグと色を更新できるようにします。 (ここでは「編集フォームにタグと色も含める」形にします)
// --- 操作ロジック:メモを編集する ---
function updateMemo(id, newTitle, newBody, newTag, newColor) {
memos.forEach(function(memo) {
if (memo.id === id) {
memo.title = newTitle;
memo.body = newBody;
memo.tag = newTag;
memo.color = newColor;
}
});
saveToStorage();
renderMemos();
}
JavaScriptメモ削除ロジック(復習)
削除ロジックはそのまま使えます。
// --- 操作ロジック:メモを削除する ---
function deleteMemo(id) {
const newMemos = memos.filter(function(memo) {
return memo.id !== id;
});
memos.length = 0;
newMemos.forEach(function(memo) {
memos.push(memo);
});
saveToStorage();
renderMemos();
}
JavaScript4日目の心臓部:タグ+色ラベル付きの描画(renderMemos)
ここが今日の一番大事な部分です。 renderMemos() の中で、
- 検索キーワードでフィルタ
- タグフィルタでさらに絞り込み
- 並び順モードでソート
- DOM を生成(タグと色ラベルを反映)
という流れを作ります。
// --- 描画:状態から画面(DOM)を作り直す ---
function renderMemos() {
memoList.innerHTML = "";
let visibleMemos = memos;
// 1. 検索キーワードでフィルタ(タイトル・本文・タグ)
if (currentKeyword.trim() !== "") {
const keywordLower = currentKeyword.toLowerCase();
visibleMemos = visibleMemos.filter(function(memo) {
const titleLower = memo.title.toLowerCase();
const bodyLower = memo.body.toLowerCase();
const tagLower = (memo.tag || "").toLowerCase();
return (
titleLower.includes(keywordLower) ||
bodyLower.includes(keywordLower) ||
tagLower.includes(keywordLower)
);
});
}
// 2. タグフィルタで絞り込み
if (currentTagFilter !== "") {
visibleMemos = visibleMemos.filter(function(memo) {
return memo.tag === currentTagFilter;
});
}
// 3. 並び替えのためにコピーを作る
visibleMemos = visibleMemos.slice();
if (currentSort === "newest") {
visibleMemos.sort(function(a, b) {
return b.createdAt - a.createdAt;
});
} else if (currentSort === "oldest") {
visibleMemos.sort(function(a, b) {
return a.createdAt - b.createdAt;
});
}
// 4. ソート済みのvisibleMemosをもとにDOMを作る
visibleMemos.forEach(function(memo) {
const memoDiv = document.createElement("div");
memoDiv.classList.add("memo-item");
memoDiv.dataset.id = String(memo.id);
// 色ラベルに応じてクラスを付ける
const colorClass = "color-" + (memo.color || "none");
memoDiv.classList.add(colorClass);
// --- 表示モード用の要素 ---
const titleDiv = document.createElement("div");
titleDiv.classList.add("memo-item-title");
titleDiv.textContent = memo.title;
const bodyDiv = document.createElement("div");
bodyDiv.classList.add("memo-item-body");
bodyDiv.textContent = memo.body;
const metaDiv = document.createElement("div");
metaDiv.classList.add("memo-meta");
metaDiv.textContent = `タグ: ${memo.tag || "(なし)"} / 色: ${memo.color || "none"}`;
// --- 編集モード用の要素 ---
const editTitleInput = document.createElement("input");
editTitleInput.type = "text";
editTitleInput.value = memo.title;
editTitleInput.style.display = "none";
const editBodyTextarea = document.createElement("textarea");
editBodyTextarea.rows = 4;
editBodyTextarea.cols = 40;
editBodyTextarea.value = memo.body;
editBodyTextarea.style.display = "none";
const editTagInput = document.createElement("input");
editTagInput.type = "text";
editTagInput.value = memo.tag || "";
editTagInput.style.display = "none";
// 色ラベル編集用のラジオボタン群(メモごとに用意)
const editColorContainer = document.createElement("div");
editColorContainer.style.display = "none";
const colors = ["none", "red", "blue", "yellow"];
const editColorRadios = [];
colors.forEach(function(color) {
const label = document.createElement("label");
const radio = document.createElement("input");
radio.type = "radio";
radio.name = "editColor-" + memo.id; // メモごとに名前を変える
radio.value = color;
if (memo.color === color || (!memo.color && color === "none")) {
radio.checked = true;
}
label.append(radio);
label.append(document.createTextNode(" " + color));
editColorContainer.append(label);
editColorRadios.push(radio);
});
// --- ボタン類 ---
const editButton = document.createElement("button");
editButton.textContent = "編集";
const saveButton = document.createElement("button");
saveButton.textContent = "保存";
saveButton.style.display = "none";
const cancelButton = document.createElement("button");
cancelButton.textContent = "キャンセル";
cancelButton.style.display = "none";
const deleteButton = document.createElement("button");
deleteButton.textContent = "削除";
// --- 編集ボタンのイベント ---
editButton.addEventListener("click", function() {
titleDiv.style.display = "none";
bodyDiv.style.display = "none";
metaDiv.style.display = "none";
editTitleInput.style.display = "block";
editBodyTextarea.style.display = "block";
editTagInput.style.display = "block";
editColorContainer.style.display = "block";
editButton.style.display = "none";
deleteButton.style.display = "none";
saveButton.style.display = "inline-block";
cancelButton.style.display = "inline-block";
memoDiv.classList.add("editing");
editTitleInput.value = memo.title;
editBodyTextarea.value = memo.body;
editTagInput.value = memo.tag || "";
editTitleInput.focus();
});
// --- 保存ボタンのイベント ---
saveButton.addEventListener("click", function() {
const newTitle = editTitleInput.value;
const newBody = editBodyTextarea.value;
const newTag = editTagInput.value;
if (newTitle.trim() === "" && newBody.trim() === "") {
alert("タイトルか本文のどちらかは入力してください。");
return;
}
// 編集用ラジオボタンから選択された色を取得
let newColor = "none";
editColorRadios.forEach(function(radio) {
if (radio.checked) {
newColor = radio.value;
}
});
const idText = memoDiv.dataset.id;
const id = Number(idText);
updateMemo(id, newTitle, newBody, newTag, newColor);
});
// --- キャンセルボタンのイベント ---
cancelButton.addEventListener("click", function() {
titleDiv.style.display = "block";
bodyDiv.style.display = "block";
metaDiv.style.display = "block";
editTitleInput.style.display = "none";
editBodyTextarea.style.display = "none";
editTagInput.style.display = "none";
editColorContainer.style.display = "none";
editButton.style.display = "inline-block";
deleteButton.style.display = "inline-block";
saveButton.style.display = "none";
cancelButton.style.display = "none";
memoDiv.classList.remove("editing");
});
// --- 削除ボタンのイベント ---
deleteButton.addEventListener("click", function(event) {
const memoElement = event.target.closest(".memo-item");
const idText = memoElement.dataset.id;
const id = Number(idText);
deleteMemo(id);
});
// --- memoDivの中に要素を追加する ---
memoDiv.append(titleDiv);
memoDiv.append(bodyDiv);
memoDiv.append(metaDiv);
memoDiv.append(editTitleInput);
memoDiv.append(editBodyTextarea);
memoDiv.append(editTagInput);
memoDiv.append(editColorContainer);
memoDiv.append(editButton);
memoDiv.append(saveButton);
memoDiv.append(cancelButton);
memoDiv.append(deleteButton);
memoList.append(memoDiv);
});
}
JavaScriptこの関数で本当に掴んでほしいこと
- タグフィルタは「状態(currentTagFilter)」で管理し、
filter()で絞り込むだけで実現できる - 色ラベルは「クラス名(color-red など)」として DOM に反映すると、 CSS で柔軟に見た目を変えられる
- 編集フォームにタグと色ラベルを含めることで、 メモの「意味」を後から変えられるようになる
検索入力欄のイベント設定(復習+タグも対象)
検索欄は、タイトル・本文・タグを対象にします。
// --- イベント設定:検索入力欄 ---
searchInput.addEventListener("input", function() {
currentKeyword = searchInput.value;
renderMemos();
});
JavaScript並び順ボタンのイベント設定(復習)
並び順ボタンは 3日目と同じパターンです。
// --- イベント設定:並び順ボタン ---
sortButtons.forEach(function(button) {
button.addEventListener("click", function() {
const sortMode = button.dataset.sort;
currentSort = sortMode;
sortButtons.forEach(function(b) {
b.classList.remove("active-sort");
});
button.classList.add("active-sort");
renderMemos();
});
});
JavaScriptタグフィルタボタンのイベント設定(querySelectorAll+dataset+classList)
ここで querySelectorAll() と dataset と classList を タグフィルタに応用します。
// --- イベント設定:タグフィルタボタン ---
tagFilterButtons.forEach(function(button) {
button.addEventListener("click", function() {
const tag = button.dataset.tag; // "" / "仕事" / "プライベート" / "アイデア"
// 状態を更新
currentTagFilter = tag;
// 全てのタグフィルタボタンから選択中クラスを外す
tagFilterButtons.forEach(function(b) {
b.classList.remove("active-tag");
});
// クリックされたボタンだけ選択中クラスを付ける
button.classList.add("active-tag");
// 新しいタグフィルタで画面を作り直す
renderMemos();
});
});
JavaScript深掘りポイント:
dataset.tagに「このボタンが表すタグ」を埋め込んでおくと、 イベントの中で簡単に取り出せる- 「すべて」ボタンは
data-tag=""として、 「タグフィルタなし」を表現する classList.add("active-tag")/classList.remove("active-tag")で、 選択中のタグを見た目で分かりやすくする
「メモを追加」ボタンのイベント設定:タグと色を渡す
追加ボタンから addMemo() にタグと色を渡します。
// --- イベント設定:メモを追加ボタン ---
addMemoButton.addEventListener("click", function() {
const title = memoTitleInput.value;
const body = memoBodyInput.value;
const tag = memoTagInput.value;
const color = getSelectedColor();
errorMessage.textContent = "";
if (title.trim() === "" && body.trim() === "") {
errorMessage.textContent = "タイトルか本文のどちらかは入力してください。";
return;
}
addMemo(title, body, tag, color);
memoTitleInput.value = "";
memoBodyInput.value = "";
memoTagInput.value = "";
// 色ラベルを「なし」に戻す
memoColorRadios.forEach(function(radio) {
radio.checked = radio.value === "none";
});
renderMemos();
});
JavaScript初期処理:保存データを読み込んでから描画
最後に、ページ読み込み時の流れを整えます。
// --- 初期処理 ---
loadFromStorage();
renderMemos();
JavaScript4日目の完成コード(JavaScript全体)
HTML の <script> タグの中身として、 ここまでのコードをひとつにまとめるとこうなります。
<script>
// --- アプリの状態(State) ---
let memos = [];
let nextMemoId = 1;
let currentKeyword = "";
let currentSort = "newest";
let currentTagFilter = "";
const STORAGE_KEY = "my_memo_app_data";
// --- DOM参照 ---
const memoTitleInput = document.querySelector("#memoTitleInput");
const memoBodyInput = document.querySelector("#memoBodyInput");
const memoTagInput = document.querySelector("#memoTagInput");
const addMemoButton = document.querySelector("#addMemoButton");
const errorMessage = document.querySelector("#errorMessage");
const memoList = document.querySelector("#memoList");
const searchInput = document.querySelector("#searchInput");
const sortButtons = document.querySelectorAll("#sortButtons button");
const tagFilterButtons = document.querySelectorAll("#tagFilterButtons button");
const memoColorRadios = document.querySelectorAll("input[name='memoColor']");
// --- ストレージ:保存 ---
function saveToStorage() {
const data = {
memos: memos,
nextMemoId: nextMemoId
};
const json = JSON.stringify(data);
localStorage.setItem(STORAGE_KEY, json);
}
// --- ストレージ:読み込み ---
function loadFromStorage() {
const json = localStorage.getItem(STORAGE_KEY);
if (json === null) {
return;
}
try {
const data = JSON.parse(json);
if (Array.isArray(data.memos) && typeof data.nextMemoId === "number") {
memos = data.memos;
nextMemoId = data.nextMemoId;
}
} catch (error) {
console.error("保存データの読み込みに失敗しました:", error);
}
}
// --- ユーティリティ:選択されている色ラベルを取得 ---
function getSelectedColor() {
let selectedValue = "none";
memoColorRadios.forEach(function(radio) {
if (radio.checked) {
selectedValue = radio.value;
}
});
return selectedValue;
}
// --- 操作ロジック:メモ追加 ---
function addMemo(title, body, tag, color) {
const newMemo = {
id: nextMemoId,
title: title,
body: body,
tag: tag,
color: color,
createdAt: Date.now()
};
nextMemoId = nextMemoId + 1;
memos.push(newMemo);
saveToStorage();
}
// --- 操作ロジック:メモ編集 ---
function updateMemo(id, newTitle, newBody, newTag, newColor) {
memos.forEach(function(memo) {
if (memo.id === id) {
memo.title = newTitle;
memo.body = newBody;
memo.tag = newTag;
memo.color = newColor;
}
});
saveToStorage();
renderMemos();
}
// --- 操作ロジック:メモ削除 ---
function deleteMemo(id) {
const newMemos = memos.filter(function(memo) {
return memo.id !== id;
});
memos.length = 0;
newMemos.forEach(function(memo) {
memos.push(memo);
});
saveToStorage();
renderMemos();
}
// --- 描画 ---
function renderMemos() {
memoList.innerHTML = "";
let visibleMemos = memos;
if (currentKeyword.trim() !== "") {
const keywordLower = currentKeyword.toLowerCase();
visibleMemos = visibleMemos.filter(function(memo) {
const titleLower = memo.title.toLowerCase();
const bodyLower = memo.body.toLowerCase();
const tagLower = (memo.tag || "").toLowerCase();
return (
titleLower.includes(keywordLower) ||
bodyLower.includes(keywordLower) ||
tagLower.includes(keywordLower)
);
});
}
if (currentTagFilter !== "") {
visibleMemos = visibleMemos.filter(function(memo) {
return memo.tag === currentTagFilter;
});
}
visibleMemos = visibleMemos.slice();
if (currentSort === "newest") {
visibleMemos.sort(function(a, b) {
return b.createdAt - a.createdAt;
});
} else if (currentSort === "oldest") {
visibleMemos.sort(function(a, b) {
return a.createdAt - b.createdAt;
});
}
visibleMemos.forEach(function(memo) {
const memoDiv = document.createElement("div");
memoDiv.classList.add("memo-item");
memoDiv.dataset.id = String(memo.id);
const colorClass = "color-" + (memo.color || "none");
memoDiv.classList.add(colorClass);
const titleDiv = document.createElement("div");
titleDiv.classList.add("memo-item-title");
titleDiv.textContent = memo.title;
const bodyDiv = document.createElement("div");
bodyDiv.classList.add("memo-item-body");
bodyDiv.textContent = memo.body;
const metaDiv = document.createElement("div");
metaDiv.classList.add("memo-meta");
metaDiv.textContent = `タグ: ${memo.tag || "(なし)"} / 色: ${memo.color || "none"}`;
const editTitleInput = document.createElement("input");
editTitleInput.type = "text";
editTitleInput.value = memo.title;
editTitleInput.style.display = "none";
const editBodyTextarea = document.createElement("textarea");
editBodyTextarea.rows = 4;
editBodyTextarea.cols = 40;
editBodyTextarea.value = memo.body;
editBodyTextarea.style.display = "none";
const editTagInput = document.createElement("input");
editTagInput.type = "text";
editTagInput.value = memo.tag || "";
editTagInput.style.display = "none";
const editColorContainer = document.createElement("div");
editColorContainer.style.display = "none";
const colors = ["none", "red", "blue", "yellow"];
const editColorRadios = [];
colors.forEach(function(color) {
const label = document.createElement("label");
const radio = document.createElement("input");
radio.type = "radio";
radio.name = "editColor-" + memo.id;
radio.value = color;
if (memo.color === color || (!memo.color && color === "none")) {
radio.checked = true;
}
label.append(radio);
label.append(document.createTextNode(" " + color));
editColorContainer.append(label);
editColorRadios.push(radio);
});
const editButton = document.createElement("button");
editButton.textContent = "編集";
const saveButton = document.createElement("button");
saveButton.textContent = "保存";
saveButton.style.display = "none";
const cancelButton = document.createElement("button");
cancelButton.textContent = "キャンセル";
cancelButton.style.display = "none";
const deleteButton = document.createElement("button");
deleteButton.textContent = "削除";
editButton.addEventListener("click", function() {
titleDiv.style.display = "none";
bodyDiv.style.display = "none";
metaDiv.style.display = "none";
editTitleInput.style.display = "block";
editBodyTextarea.style.display = "block";
editTagInput.style.display = "block";
editColorContainer.style.display = "block";
editButton.style.display = "none";
deleteButton.style.display = "none";
saveButton.style.display = "inline-block";
cancelButton.style.display = "inline-block";
memoDiv.classList.add("editing");
editTitleInput.value = memo.title;
editBodyTextarea.value = memo.body;
editTagInput.value = memo.tag || "";
editTitleInput.focus();
});
saveButton.addEventListener("click", function() {
const newTitle = editTitleInput.value;
const newBody = editBodyTextarea.value;
const newTag = editTagInput.value;
if (newTitle.trim() === "" && newBody.trim() === "") {
alert("タイトルか本文のどちらかは入力してください。");
return;
}
let newColor = "none";
editColorRadios.forEach(function(radio) {
if (radio.checked) {
newColor = radio.value;
}
});
const idText = memoDiv.dataset.id;
const id = Number(idText);
updateMemo(id, newTitle, newBody, newTag, newColor);
});
cancelButton.addEventListener("click", function() {
titleDiv.style.display = "block";
bodyDiv.style.display = "block";
metaDiv.style.display = "block";
editTitleInput.style.display = "none";
editBodyTextarea.style.display = "none";
editTagInput.style.display = "none";
editColorContainer.style.display = "none";
editButton.style.display = "inline-block";
deleteButton.style.display = "inline-block";
saveButton.style.display = "none";
cancelButton.style.display = "none";
memoDiv.classList.remove("editing");
});
deleteButton.addEventListener("click", function(event) {
const memoElement = event.target.closest(".memo-item");
const idText = memoElement.dataset.id;
const id = Number(idText);
deleteMemo(id);
});
memoDiv.append(titleDiv);
memoDiv.append(bodyDiv);
memoDiv.append(metaDiv);
memoDiv.append(editTitleInput);
memoDiv.append(editBodyTextarea);
memoDiv.append(editTagInput);
memoDiv.append(editColorContainer);
memoDiv.append(editButton);
memoDiv.append(saveButton);
memoDiv.append(cancelButton);
memoDiv.append(deleteButton);
memoList.append(memoDiv);
});
}
// --- イベント設定:メモ追加ボタン ---
addMemoButton.addEventListener("click", function() {
const title = memoTitleInput.value;
const body = memoBodyInput.value;
const tag = memoTagInput.value;
const color = getSelectedColor();
errorMessage.textContent = "";
if (title.trim() === "" && body.trim() === "") {
errorMessage.textContent = "タイトルか本文のどちらかは入力してください。";
return;
}
addMemo(title, body, tag, color);
memoTitleInput.value = "";
memoBodyInput.value = "";
memoTagInput.value = "";
memoColorRadios.forEach(function(radio) {
radio.checked = radio.value === "none";
});
renderMemos();
});
// --- イベント設定:検索入力欄 ---
searchInput.addEventListener("input", function() {
currentKeyword = searchInput.value;
renderMemos();
});
// --- イベント設定:並び順ボタン ---
sortButtons.forEach(function(button) {
button.addEventListener("click", function() {
const sortMode = button.dataset.sort;
currentSort = sortMode;
sortButtons.forEach(function(b) {
b.classList.remove("active-sort");
});
button.classList.add("active-sort");
renderMemos();
});
});
// --- イベント設定:タグフィルタボタン ---
tagFilterButtons.forEach(function(button) {
button.addEventListener("click", function() {
const tag = button.dataset.tag;
currentTagFilter = tag;
tagFilterButtons.forEach(function(b) {
b.classList.remove("active-tag");
});
button.classList.add("active-tag");
renderMemos();
});
});
// --- 初期処理 ---
loadFromStorage();
renderMemos();
</script>
4日目で本当に掴んでほしいこと
- タグや色ラベルのような「意味」を状態として持つと、 メモ帳が一気に“自分の生活にフィットするツール”になる
querySelectorAll()+dataset+classListのパターンは、 並び順ボタンだけでなくタグフィルタにもそのまま応用できる- 配列(状態)→ localStorage(保存)→ DOM(描画)→ イベント(操作)という流れの上に、 「タグ」「色」「フィルタ」「検索」「並び替え」が自然に積み上がっている
ここまで来ると、 ToDoアプリでも、メモ帳でも、家計簿でも、 「自分のルールで整理できるアプリ」を 自分の手で設計していけるようになります。
メモ帳アプリ 5日目のゴール
5日目のテーマは 「メモの“重要度”や“状態”を管理できるメモ帳に育てる」 ことです。
1〜4日目で作ったメモ帳アプリは、
- 追加・編集・削除
- 検索
- 並び替え(新しい順/古い順)
- タグ・色ラベル
- localStorage に保存
までできる状態でした。
5日目ではここから一歩進めて、
- メモを「ピン留め(お気に入り)」できる
- メモを「アーカイブ(非表示)」できる
- ピン留めメモを上に表示する
- アーカイブメモをフィルタで切り替える
datasetとclassListを使って「状態付きのUI」を作る
というところまでを、ステップバイステップで作っていきます。
今日の全体ステップ
5日目の流れはこんな感じです。
- メモオブジェクトに
pinnedとarchivedプロパティを追加する - メモ一覧の表示に「ピンボタン」と「アーカイブボタン」を追加する
- ピン状態に応じてクラスを付け、見た目を変える
- アーカイブ状態に応じて表示/非表示を切り替える
- 「表示モード(通常/アーカイブ含む/アーカイブのみ)」をフィルタボタンで切り替える
querySelectorAll()+dataset+classListでフィルタボタンを制御する
5日目の HTML レイアウト
4日目までの HTML に、 「表示モード切り替え」の UI を追加します。 (タグフィルタはそのまま使ってもいいですし、いったん省略しても構いません)
ここでは、タグフィルタは残したまま、 新しく「表示モードフィルタ」を追加する形で説明します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>メモ帳アプリ 5日目</title>
<style>
.memo-item {
border: 1px solid #ccc;
padding: 8px;
margin-bottom: 8px;
}
.memo-item-title {
font-weight: bold;
margin-bottom: 4px;
}
.memo-item-body {
white-space: pre-wrap;
}
.memo-meta {
font-size: 12px;
color: #666;
margin-top: 4px;
}
.editing {
background-color: #fff7e6;
}
.active-sort {
background-color: #007bff;
color: white;
}
.active-tag {
background-color: #28a745;
color: white;
}
.active-view {
background-color: #6f42c1;
color: white;
}
.color-red {
border-left: 4px solid #ff4d4f;
}
.color-blue {
border-left: 4px solid #1890ff;
}
.color-yellow {
border-left: 4px solid #faad14;
}
.color-none {
border-left: 4px solid transparent;
}
/* ピン留めされたメモの見た目 */
.pinned {
border-top: 3px solid #ffc107;
background-color: #fffbe6;
}
/* アーカイブされたメモの見た目(一覧に表示する場合) */
.archived {
opacity: 0.6;
}
</style>
</head>
<body>
<h1>メモ帳アプリ(5日目)</h1>
<div>
<label for="memoTitleInput">タイトル:</label>
<input id="memoTitleInput" type="text" placeholder="メモのタイトルを入力してください">
</div>
<div>
<label for="memoBodyInput">本文:</label><br>
<textarea id="memoBodyInput" rows="4" cols="40" placeholder="メモの本文を入力してください"></textarea>
</div>
<div>
<label for="memoTagInput">タグ:</label>
<input id="memoTagInput" type="text" placeholder="例:仕事 / プライベート / アイデア">
</div>
<div>
<span>色ラベル:</span>
<label>
<input type="radio" name="memoColor" value="none" checked> なし
</label>
<label>
<input type="radio" name="memoColor" value="red"> 赤
</label>
<label>
<input type="radio" name="memoColor" value="blue"> 青
</label>
<label>
<input type="radio" name="memoColor" value="yellow"> 黄
</label>
</div>
<div>
<button id="addMemoButton">メモを追加</button>
</div>
<p id="errorMessage" style="color: red;"></p>
<h2>検索</h2>
<div>
<input id="searchInput" type="text" placeholder="タイトル・本文・タグから検索">
</div>
<h2>並び順</h2>
<div id="sortButtons">
<button data-sort="newest" class="active-sort">新しい順</button>
<button data-sort="oldest">古い順</button>
</div>
<h2>タグフィルタ</h2>
<div id="tagFilterButtons">
<button data-tag="" class="active-tag">すべて</button>
<button data-tag="仕事">仕事</button>
<button data-tag="プライベート">プライベート</button>
<button data-tag="アイデア">アイデア</button>
</div>
<h2>表示モード</h2>
<div id="viewFilterButtons">
<button data-view="all" class="active-view">通常(アーカイブ以外)</button>
<button data-view="withArchived">アーカイブも含める</button>
<button data-view="archivedOnly">アーカイブのみ</button>
</div>
<h2>メモ一覧</h2>
<div id="memoList"></div>
<script>
// ここに JavaScript を書いていきます
</script>
</body>
</html>
ここで押さえておきたいポイント
- 新しく
#viewFilterButtonsを追加し、 各ボタンにdata-view属性で表示モードを埋め込む"all"… アーカイブ以外"withArchived"… アーカイブも含めて表示"archivedOnly"… アーカイブだけ表示
- ピン留めされたメモには
.pinnedクラス、 アーカイブされたメモには.archivedクラスを付けて見た目を変える
状態と DOM 参照の定義
5日目では、 「表示モード」と「ピン/アーカイブ状態」を扱います。
// --- アプリの状態(State) ---
let memos = [];
let nextMemoId = 1;
// 検索キーワード
let currentKeyword = "";
// 並び順モード("newest" / "oldest")
let currentSort = "newest";
// タグフィルタ(空文字なら「すべて」)
let currentTagFilter = "";
// 表示モード("all" / "withArchived" / "archivedOnly")
let currentViewMode = "all";
const STORAGE_KEY = "my_memo_app_data";
// --- DOM参照 ---
const memoTitleInput = document.querySelector("#memoTitleInput");
const memoBodyInput = document.querySelector("#memoBodyInput");
const memoTagInput = document.querySelector("#memoTagInput");
const addMemoButton = document.querySelector("#addMemoButton");
const errorMessage = document.querySelector("#errorMessage");
const memoList = document.querySelector("#memoList");
const searchInput = document.querySelector("#searchInput");
const sortButtons = document.querySelectorAll("#sortButtons button");
const tagFilterButtons = document.querySelectorAll("#tagFilterButtons button");
const viewFilterButtons = document.querySelectorAll("#viewFilterButtons button");
const memoColorRadios = document.querySelectorAll("input[name='memoColor']");
JavaScript深掘りポイント:
- 「表示モード」も立派な状態
- ピン/アーカイブは「メモごとのプロパティ」として持たせる
querySelectorAll()で複数ボタンをまとめて扱うパターンが どんどん再利用されている
localStorage の保存・読み込み
メモオブジェクトにプロパティが増えても、 JSON はそのまま保存・読み込みしてくれます。
// --- ストレージ:状態をlocalStorageに保存する ---
function saveToStorage() {
const data = {
memos: memos,
nextMemoId: nextMemoId
};
const json = JSON.stringify(data);
localStorage.setItem(STORAGE_KEY, json);
}
// --- ストレージ:localStorageから状態を読み込む ---
function loadFromStorage() {
const json = localStorage.getItem(STORAGE_KEY);
if (json === null) {
return;
}
try {
const data = JSON.parse(json);
if (Array.isArray(data.memos) && typeof data.nextMemoId === "number") {
memos = data.memos;
nextMemoId = data.nextMemoId;
}
} catch (error) {
console.error("保存データの読み込みに失敗しました:", error);
}
}
JavaScript色ラベルの選択値を取得する関数(復習)
// --- ユーティリティ:選択されている色ラベルを取得する ---
function getSelectedColor() {
let selectedValue = "none";
memoColorRadios.forEach(function(radio) {
if (radio.checked) {
selectedValue = radio.value;
}
});
return selectedValue;
}
JavaScriptメモ追加ロジック:ピン/アーカイブの初期値を持たせる
新しく追加するメモは、 最初は「ピンなし」「アーカイブなし」にします。
// --- 操作ロジック:メモを追加する ---
function addMemo(title, body, tag, color) {
const newMemo = {
id: nextMemoId,
title: title,
body: body,
tag: tag,
color: color,
pinned: false, // ピン留めされているかどうか
archived: false, // アーカイブされているかどうか
createdAt: Date.now()
};
nextMemoId = nextMemoId + 1;
memos.push(newMemo);
saveToStorage();
}
JavaScript深掘りポイント:
- 「ピン留め」「アーカイブ」は 真偽値(
true/false)で持つと扱いやすい - 初期値は
falseにしておき、 ボタン操作でtrue/falseを切り替える
メモ編集ロジック:タグ・色はそのまま、ピン/アーカイブは別操作にする
編集フォームではタイトル・本文・タグ・色を編集し、 ピン/アーカイブは専用ボタンで切り替える形にします。
// --- 操作ロジック:メモを編集する ---
function updateMemo(id, newTitle, newBody, newTag, newColor) {
memos.forEach(function(memo) {
if (memo.id === id) {
memo.title = newTitle;
memo.body = newBody;
memo.tag = newTag;
memo.color = newColor;
}
});
saveToStorage();
renderMemos();
}
JavaScriptピン状態を切り替えるロジック
// --- 操作ロジック:ピン状態を切り替える ---
function togglePinned(id) {
memos.forEach(function(memo) {
if (memo.id === id) {
memo.pinned = !memo.pinned; // true/falseを反転
}
});
saveToStorage();
renderMemos();
}
JavaScriptアーカイブ状態を切り替えるロジック
// --- 操作ロジック:アーカイブ状態を切り替える ---
function toggleArchived(id) {
memos.forEach(function(memo) {
if (memo.id === id) {
memo.archived = !memo.archived; // true/falseを反転
}
});
saveToStorage();
renderMemos();
}
JavaScript深掘りポイント:
- 「状態を反転する」パターンは
memo.pinned = !memo.pinnedのように書ける - ピン/アーカイブは「編集」とは別の操作として分けておくと、 UI が分かりやすくなる
メモ削除ロジック(復習)
// --- 操作ロジック:メモを削除する ---
function deleteMemo(id) {
const newMemos = memos.filter(function(memo) {
return memo.id !== id;
});
memos.length = 0;
newMemos.forEach(function(memo) {
memos.push(memo);
});
saveToStorage();
renderMemos();
}
JavaScript5日目の心臓部:ピン+アーカイブ+表示モード付きの描画(renderMemos)
ここが今日の一番大事な部分です。 renderMemos() の中で、
- 検索キーワードでフィルタ
- タグフィルタで絞り込み
- 表示モード(アーカイブの扱い)でさらに絞り込み
- 並び順モードでソート(ピン留めを優先)
- DOM を生成(ピン/アーカイブを見た目に反映)
という流れを作ります。
// --- 描画:状態から画面(DOM)を作り直す ---
function renderMemos() {
memoList.innerHTML = "";
let visibleMemos = memos;
// 1. 検索キーワードでフィルタ(タイトル・本文・タグ)
if (currentKeyword.trim() !== "") {
const keywordLower = currentKeyword.toLowerCase();
visibleMemos = visibleMemos.filter(function(memo) {
const titleLower = memo.title.toLowerCase();
const bodyLower = memo.body.toLowerCase();
const tagLower = (memo.tag || "").toLowerCase();
return (
titleLower.includes(keywordLower) ||
bodyLower.includes(keywordLower) ||
tagLower.includes(keywordLower)
);
});
}
// 2. タグフィルタで絞り込み
if (currentTagFilter !== "") {
visibleMemos = visibleMemos.filter(function(memo) {
return memo.tag === currentTagFilter;
});
}
// 3. 表示モードで絞り込み
if (currentViewMode === "all") {
// アーカイブされていないメモだけ表示
visibleMemos = visibleMemos.filter(function(memo) {
return memo.archived === false;
});
} else if (currentViewMode === "archivedOnly") {
// アーカイブされたメモだけ表示
visibleMemos = visibleMemos.filter(function(memo) {
return memo.archived === true;
});
} else if (currentViewMode === "withArchived") {
// 何もしない(アーカイブも含めて表示)
}
// 4. 並び替えのためにコピーを作る
visibleMemos = visibleMemos.slice();
// ピン留めされたメモを優先して並べる
visibleMemos.sort(function(a, b) {
// まずピン状態で比較(trueを前に)
if (a.pinned && !b.pinned) {
return -1;
}
if (!a.pinned && b.pinned) {
return 1;
}
// ピン状態が同じなら、createdAtで比較
if (currentSort === "newest") {
return b.createdAt - a.createdAt;
} else {
return a.createdAt - b.createdAt;
}
});
// 5. ソート済みのvisibleMemosをもとにDOMを作る
visibleMemos.forEach(function(memo) {
const memoDiv = document.createElement("div");
memoDiv.classList.add("memo-item");
memoDiv.dataset.id = String(memo.id);
// 色ラベルに応じてクラスを付ける
const colorClass = "color-" + (memo.color || "none");
memoDiv.classList.add(colorClass);
// ピン状態に応じてクラスを付ける
if (memo.pinned) {
memoDiv.classList.add("pinned");
}
// アーカイブ状態に応じてクラスを付ける
if (memo.archived) {
memoDiv.classList.add("archived");
}
// --- 表示モード用の要素 ---
const titleDiv = document.createElement("div");
titleDiv.classList.add("memo-item-title");
titleDiv.textContent = memo.title;
const bodyDiv = document.createElement("div");
bodyDiv.classList.add("memo-item-body");
bodyDiv.textContent = memo.body;
const metaDiv = document.createElement("div");
metaDiv.classList.add("memo-meta");
metaDiv.textContent =
`タグ: ${memo.tag || "(なし)"} / 色: ${memo.color || "none"} / ` +
`ピン: ${memo.pinned ? "ON" : "OFF"} / アーカイブ: ${memo.archived ? "ON" : "OFF"}`;
// --- 編集モード用の要素 ---
const editTitleInput = document.createElement("input");
editTitleInput.type = "text";
editTitleInput.value = memo.title;
editTitleInput.style.display = "none";
const editBodyTextarea = document.createElement("textarea");
editBodyTextarea.rows = 4;
editBodyTextarea.cols = 40;
editBodyTextarea.value = memo.body;
editBodyTextarea.style.display = "none";
const editTagInput = document.createElement("input");
editTagInput.type = "text";
editTagInput.value = memo.tag || "";
editTagInput.style.display = "none";
const editColorContainer = document.createElement("div");
editColorContainer.style.display = "none";
const colors = ["none", "red", "blue", "yellow"];
const editColorRadios = [];
colors.forEach(function(color) {
const label = document.createElement("label");
const radio = document.createElement("input");
radio.type = "radio";
radio.name = "editColor-" + memo.id;
radio.value = color;
if (memo.color === color || (!memo.color && color === "none")) {
radio.checked = true;
}
label.append(radio);
label.append(document.createTextNode(" " + color));
editColorContainer.append(label);
editColorRadios.push(radio);
});
// --- ボタン類 ---
const editButton = document.createElement("button");
editButton.textContent = "編集";
const saveButton = document.createElement("button");
saveButton.textContent = "保存";
saveButton.style.display = "none";
const cancelButton = document.createElement("button");
cancelButton.textContent = "キャンセル";
cancelButton.style.display = "none";
const deleteButton = document.createElement("button");
deleteButton.textContent = "削除";
const pinButton = document.createElement("button");
pinButton.textContent = memo.pinned ? "ピン解除" : "ピン";
const archiveButton = document.createElement("button");
archiveButton.textContent = memo.archived ? "アーカイブ解除" : "アーカイブ";
// --- 編集ボタンのイベント ---
editButton.addEventListener("click", function() {
titleDiv.style.display = "none";
bodyDiv.style.display = "none";
metaDiv.style.display = "none";
editTitleInput.style.display = "block";
editBodyTextarea.style.display = "block";
editTagInput.style.display = "block";
editColorContainer.style.display = "block";
editButton.style.display = "none";
deleteButton.style.display = "none";
pinButton.style.display = "none";
archiveButton.style.display = "none";
saveButton.style.display = "inline-block";
cancelButton.style.display = "inline-block";
memoDiv.classList.add("editing");
editTitleInput.value = memo.title;
editBodyTextarea.value = memo.body;
editTagInput.value = memo.tag || "";
editTitleInput.focus();
});
// --- 保存ボタンのイベント ---
saveButton.addEventListener("click", function() {
const newTitle = editTitleInput.value;
const newBody = editBodyTextarea.value;
const newTag = editTagInput.value;
if (newTitle.trim() === "" && newBody.trim() === "") {
alert("タイトルか本文のどちらかは入力してください。");
return;
}
let newColor = "none";
editColorRadios.forEach(function(radio) {
if (radio.checked) {
newColor = radio.value;
}
});
const idText = memoDiv.dataset.id;
const id = Number(idText);
updateMemo(id, newTitle, newBody, newTag, newColor);
});
// --- キャンセルボタンのイベント ---
cancelButton.addEventListener("click", function() {
titleDiv.style.display = "block";
bodyDiv.style.display = "block";
metaDiv.style.display = "block";
editTitleInput.style.display = "none";
editBodyTextarea.style.display = "none";
editTagInput.style.display = "none";
editColorContainer.style.display = "none";
editButton.style.display = "inline-block";
deleteButton.style.display = "inline-block";
pinButton.style.display = "inline-block";
archiveButton.style.display = "inline-block";
saveButton.style.display = "none";
cancelButton.style.display = "none";
memoDiv.classList.remove("editing");
});
// --- 削除ボタンのイベント ---
deleteButton.addEventListener("click", function(event) {
const memoElement = event.target.closest(".memo-item");
const idText = memoElement.dataset.id;
const id = Number(idText);
deleteMemo(id);
});
// --- ピンボタンのイベント ---
pinButton.addEventListener("click", function(event) {
const memoElement = event.target.closest(".memo-item");
const idText = memoElement.dataset.id;
const id = Number(idText);
togglePinned(id);
});
// --- アーカイブボタンのイベント ---
archiveButton.addEventListener("click", function(event) {
const memoElement = event.target.closest(".memo-item");
const idText = memoElement.dataset.id;
const id = Number(idText);
toggleArchived(id);
});
// --- memoDivの中に要素を追加する ---
memoDiv.append(titleDiv);
memoDiv.append(bodyDiv);
memoDiv.append(metaDiv);
memoDiv.append(editTitleInput);
memoDiv.append(editBodyTextarea);
memoDiv.append(editTagInput);
memoDiv.append(editColorContainer);
memoDiv.append(editButton);
memoDiv.append(saveButton);
memoDiv.append(cancelButton);
memoDiv.append(deleteButton);
memoDiv.append(pinButton);
memoDiv.append(archiveButton);
memoList.append(memoDiv);
});
}
JavaScriptこの関数で本当に掴んでほしいこと
- 表示モード(currentViewMode)を状態として持ち、
filter()でアーカイブの扱いを切り替えるだけで 「通常/アーカイブ含む/アーカイブのみ」が実現できる - ピン留めは「ソートの優先度」として扱うと、 自然に「上に来る」挙動を作れる
- ピン/アーカイブは
classListで見た目に反映し、 ボタンのラベル(ピン/ピン解除など)も状態に応じて変える
検索入力欄のイベント設定(復習)
// --- イベント設定:検索入力欄 ---
searchInput.addEventListener("input", function() {
currentKeyword = searchInput.value;
renderMemos();
});
JavaScript並び順ボタンのイベント設定(復習)
// --- イベント設定:並び順ボタン ---
sortButtons.forEach(function(button) {
button.addEventListener("click", function() {
const sortMode = button.dataset.sort;
currentSort = sortMode;
sortButtons.forEach(function(b) {
b.classList.remove("active-sort");
});
button.classList.add("active-sort");
renderMemos();
});
});
JavaScriptタグフィルタボタンのイベント設定(復習)
// --- イベント設定:タグフィルタボタン ---
tagFilterButtons.forEach(function(button) {
button.addEventListener("click", function() {
const tag = button.dataset.tag;
currentTagFilter = tag;
tagFilterButtons.forEach(function(b) {
b.classList.remove("active-tag");
});
button.classList.add("active-tag");
renderMemos();
});
});
JavaScript表示モードフィルタボタンのイベント設定
ここで querySelectorAll()+dataset+classList を 表示モードにも応用します。
// --- イベント設定:表示モードフィルタボタン ---
viewFilterButtons.forEach(function(button) {
button.addEventListener("click", function() {
const viewMode = button.dataset.view; // "all" / "withArchived" / "archivedOnly"
currentViewMode = viewMode;
viewFilterButtons.forEach(function(b) {
b.classList.remove("active-view");
});
button.classList.add("active-view");
renderMemos();
});
});
JavaScript深掘りポイント:
dataset.viewに「このボタンが表す表示モード」を埋め込んでおくと、 イベントの中で簡単に取り出せるclassList.add("active-view")/classList.remove("active-view")で、 選択中の表示モードを見た目で分かりやすくする- 並び順ボタン・タグフィルタボタン・表示モードボタンが すべて同じパターンで書けていることに注目してほしい
「メモを追加」ボタンのイベント設定
// --- イベント設定:メモを追加ボタン ---
addMemoButton.addEventListener("click", function() {
const title = memoTitleInput.value;
const body = memoBodyInput.value;
const tag = memoTagInput.value;
const color = getSelectedColor();
errorMessage.textContent = "";
if (title.trim() === "" && body.trim() === "") {
errorMessage.textContent = "タイトルか本文のどちらかは入力してください。";
return;
}
addMemo(title, body, tag, color);
memoTitleInput.value = "";
memoBodyInput.value = "";
memoTagInput.value = "";
memoColorRadios.forEach(function(radio) {
radio.checked = radio.value === "none";
});
renderMemos();
});
JavaScript初期処理:保存データを読み込んでから描画
// --- 初期処理 ---
loadFromStorage();
renderMemos();
JavaScript5日目の完成コード(JavaScript全体)
HTML の <script> タグの中身として、 ここまでのコードをひとつにまとめるとこうなります。
<script>
// --- アプリの状態(State) ---
let memos = [];
let nextMemoId = 1;
let currentKeyword = "";
let currentSort = "newest";
let currentTagFilter = "";
let currentViewMode = "all";
const STORAGE_KEY = "my_memo_app_data";
// --- DOM参照 ---
const memoTitleInput = document.querySelector("#memoTitleInput");
const memoBodyInput = document.querySelector("#memoBodyInput");
const memoTagInput = document.querySelector("#memoTagInput");
const addMemoButton = document.querySelector("#addMemoButton");
const errorMessage = document.querySelector("#errorMessage");
const memoList = document.querySelector("#memoList");
const searchInput = document.querySelector("#searchInput");
const sortButtons = document.querySelectorAll("#sortButtons button");
const tagFilterButtons = document.querySelectorAll("#tagFilterButtons button");
const viewFilterButtons = document.querySelectorAll("#viewFilterButtons button");
const memoColorRadios = document.querySelectorAll("input[name='memoColor']");
// --- ストレージ:保存 ---
function saveToStorage() {
const data = {
memos: memos,
nextMemoId: nextMemoId
};
const json = JSON.stringify(data);
localStorage.setItem(STORAGE_KEY, json);
}
// --- ストレージ:読み込み ---
function loadFromStorage() {
const json = localStorage.getItem(STORAGE_KEY);
if (json === null) {
return;
}
try {
const data = JSON.parse(json);
if (Array.isArray(data.memos) && typeof data.nextMemoId === "number") {
memos = data.memos;
nextMemoId = data.nextMemoId;
}
} catch (error) {
console.error("保存データの読み込みに失敗しました:", error);
}
}
// --- ユーティリティ:選択されている色ラベルを取得 ---
function getSelectedColor() {
let selectedValue = "none";
memoColorRadios.forEach(function(radio) {
if (radio.checked) {
selectedValue = radio.value;
}
});
return selectedValue;
}
// --- 操作ロジック:メモ追加 ---
function addMemo(title, body, tag, color) {
const newMemo = {
id: nextMemoId,
title: title,
body: body,
tag: tag,
color: color,
pinned: false,
archived: false,
createdAt: Date.now()
};
nextMemoId = nextMemoId + 1;
memos.push(newMemo);
saveToStorage();
}
// --- 操作ロジック:メモ編集 ---
function updateMemo(id, newTitle, newBody, newTag, newColor) {
memos.forEach(function(memo) {
if (memo.id === id) {
memo.title = newTitle;
memo.body = newBody;
memo.tag = newTag;
memo.color = newColor;
}
});
saveToStorage();
renderMemos();
}
// --- 操作ロジック:ピン状態切り替え ---
function togglePinned(id) {
memos.forEach(function(memo) {
if (memo.id === id) {
memo.pinned = !memo.pinned;
}
});
saveToStorage();
renderMemos();
}
// --- 操作ロジック:アーカイブ状態切り替え ---
function toggleArchived(id) {
memos.forEach(function(memo) {
if (memo.id === id) {
memo.archived = !memo.archived;
}
});
saveToStorage();
renderMemos();
}
// --- 操作ロジック:メモ削除 ---
function deleteMemo(id) {
const newMemos = memos.filter(function(memo) {
return memo.id !== id;
});
memos.length = 0;
newMemos.forEach(function(memo) {
memos.push(memo);
});
saveToStorage();
renderMemos();
}
// --- 描画 ---
function renderMemos() {
memoList.innerHTML = "";
let visibleMemos = memos;
if (currentKeyword.trim() !== "") {
const keywordLower = currentKeyword.toLowerCase();
visibleMemos = visibleMemos.filter(function(memo) {
const titleLower = memo.title.toLowerCase();
const bodyLower = memo.body.toLowerCase();
const tagLower = (memo.tag || "").toLowerCase();
return (
titleLower.includes(keywordLower) ||
bodyLower.includes(keywordLower) ||
tagLower.includes(keywordLower)
);
});
}
if (currentTagFilter !== "") {
visibleMemos = visibleMemos.filter(function(memo) {
return memo.tag === currentTagFilter;
});
}
if (currentViewMode === "all") {
visibleMemos = visibleMemos.filter(function(memo) {
return memo.archived === false;
});
} else if (currentViewMode === "archivedOnly") {
visibleMemos = visibleMemos.filter(function(memo) {
return memo.archived === true;
});
}
visibleMemos = visibleMemos.slice();
visibleMemos.sort(function(a, b) {
if (a.pinned && !b.pinned) {
return -1;
}
if (!a.pinned && b.pinned) {
return 1;
}
if (currentSort === "newest") {
return b.createdAt - a.createdAt;
} else {
return a.createdAt - b.createdAt;
}
});
visibleMemos.forEach(function(memo) {
const memoDiv = document.createElement("div");
memoDiv.classList.add("memo-item");
memoDiv.dataset.id = String(memo.id);
const colorClass = "color-" + (memo.color || "none");
memoDiv.classList.add(colorClass);
if (memo.pinned) {
memoDiv.classList.add("pinned");
}
if (memo.archived) {
memoDiv.classList.add("archived");
}
const titleDiv = document.createElement("div");
titleDiv.classList.add("memo-item-title");
titleDiv.textContent = memo.title;
const bodyDiv = document.createElement("div");
bodyDiv.classList.add("memo-item-body");
bodyDiv.textContent = memo.body;
const metaDiv = document.createElement("div");
metaDiv.classList.add("memo-meta");
metaDiv.textContent =
`タグ: ${memo.tag || "(なし)"} / 色: ${memo.color || "none"} / ` +
`ピン: ${memo.pinned ? "ON" : "OFF"} / アーカイブ: ${memo.archived ? "ON" : "OFF"}`;
const editTitleInput = document.createElement("input");
editTitleInput.type = "text";
editTitleInput.value = memo.title;
editTitleInput.style.display = "none";
const editBodyTextarea = document.createElement("textarea");
editBodyTextarea.rows = 4;
editBodyTextarea.cols = 40;
editBodyTextarea.value = memo.body;
editBodyTextarea.style.display = "none";
const editTagInput = document.createElement("input");
editTagInput.type = "text";
editTagInput.value = memo.tag || "";
editTagInput.style.display = "none";
const editColorContainer = document.createElement("div");
editColorContainer.style.display = "none";
const colors = ["none", "red", "blue", "yellow"];
const editColorRadios = [];
colors.forEach(function(color) {
const label = document.createElement("label");
const radio = document.createElement("input");
radio.type = "radio";
radio.name = "editColor-" + memo.id;
radio.value = color;
if (memo.color === color || (!memo.color && color === "none")) {
radio.checked = true;
}
label.append(radio);
label.append(document.createTextNode(" " + color));
editColorContainer.append(label);
editColorRadios.push(radio);
});
const editButton = document.createElement("button");
editButton.textContent = "編集";
const saveButton = document.createElement("button");
saveButton.textContent = "保存";
saveButton.style.display = "none";
const cancelButton = document.createElement("button");
cancelButton.textContent = "キャンセル";
cancelButton.style.display = "none";
const deleteButton = document.createElement("button");
deleteButton.textContent = "削除";
const pinButton = document.createElement("button");
pinButton.textContent = memo.pinned ? "ピン解除" : "ピン";
const archiveButton = document.createElement("button");
archiveButton.textContent = memo.archived ? "アーカイブ解除" : "アーカイブ";
editButton.addEventListener("click", function() {
titleDiv.style.display = "none";
bodyDiv.style.display = "none";
metaDiv.style.display = "none";
editTitleInput.style.display = "block";
editBodyTextarea.style.display = "block";
editTagInput.style.display = "block";
editColorContainer.style.display = "block";
editButton.style.display = "none";
deleteButton.style.display = "none";
pinButton.style.display = "none";
archiveButton.style.display = "none";
saveButton.style.display = "inline-block";
cancelButton.style.display = "inline-block";
memoDiv.classList.add("editing");
editTitleInput.value = memo.title;
editBodyTextarea.value = memo.body;
editTagInput.value = memo.tag || "";
editTitleInput.focus();
});
saveButton.addEventListener("click", function() {
const newTitle = editTitleInput.value;
const newBody = editBodyTextarea.value;
const newTag = editTagInput.value;
if (newTitle.trim() === "" && newBody.trim() === "") {
alert("タイトルか本文のどちらかは入力してください。");
return;
}
let newColor = "none";
editColorRadios.forEach(function(radio) {
if (radio.checked) {
newColor = radio.value;
}
});
const idText = memoDiv.dataset.id;
const id = Number(idText);
updateMemo(id, newTitle, newBody, newTag, newColor);
});
cancelButton.addEventListener("click", function() {
titleDiv.style.display = "block";
bodyDiv.style.display = "block";
metaDiv.style.display = "block";
editTitleInput.style.display = "none";
editBodyTextarea.style.display = "none";
editTagInput.style.display = "none";
editColorContainer.style.display = "none";
editButton.style.display = "inline-block";
deleteButton.style.display = "inline-block";
pinButton.style.display = "inline-block";
archiveButton.style.display = "inline-block";
saveButton.style.display = "none";
cancelButton.style.display = "none";
memoDiv.classList.remove("editing");
});
deleteButton.addEventListener("click", function(event) {
const memoElement = event.target.closest(".memo-item");
const idText = memoElement.dataset.id;
const id = Number(idText);
deleteMemo(id);
});
pinButton.addEventListener("click", function(event) {
const memoElement = event.target.closest(".memo-item");
const idText = memoElement.dataset.id;
const id = Number(idText);
togglePinned(id);
});
archiveButton.addEventListener("click", function(event) {
const memoElement = event.target.closest(".memo-item");
const idText = memoElement.dataset.id;
const id = Number(idText);
toggleArchived(id);
});
memoDiv.append(titleDiv);
memoDiv.append(bodyDiv);
memoDiv.append(metaDiv);
memoDiv.append(editTitleInput);
memoDiv.append(editBodyTextarea);
memoDiv.append(editTagInput);
memoDiv.append(editColorContainer);
memoDiv.append(editButton);
memoDiv.append(saveButton);
memoDiv.append(cancelButton);
memoDiv.append(deleteButton);
memoDiv.append(pinButton);
memoDiv.append(archiveButton);
memoList.append(memoDiv);
});
}
// --- イベント設定:メモ追加ボタン ---
addMemoButton.addEventListener("click", function() {
const title = memoTitleInput.value;
const body = memoBodyInput.value;
const tag = memoTagInput.value;
const color = getSelectedColor();
errorMessage.textContent = "";
if (title.trim() === "" && body.trim() === "") {
errorMessage.textContent = "タイトルか本文のどちらかは入力してください。";
return;
}
addMemo(title, body, tag, color);
memoTitleInput.value = "";
memoBodyInput.value = "";
memoTagInput.value = "";
memoColorRadios.forEach(function(radio) {
radio.checked = radio.value === "none";
});
renderMemos();
});
// --- イベント設定:検索入力欄 ---
searchInput.addEventListener("input", function() {
currentKeyword = searchInput.value;
renderMemos();
});
// --- イベント設定:並び順ボタン ---
sortButtons.forEach(function(button) {
button.addEventListener("click", function() {
const sortMode = button.dataset.sort;
currentSort = sortMode;
sortButtons.forEach(function(b) {
b.classList.remove("active-sort");
});
button.classList.add("active-sort");
renderMemos();
});
});
// --- イベント設定:タグフィルタボタン ---
tagFilterButtons.forEach(function(button) {
button.addEventListener("click", function() {
const tag = button.dataset.tag;
currentTagFilter = tag;
tagFilterButtons.forEach(function(b) {
b.classList.remove("active-tag");
});
button.classList.add("active-tag");
renderMemos();
});
});
// --- イベント設定:表示モードフィルタボタン ---
viewFilterButtons.forEach(function(button) {
button.addEventListener("click", function() {
const viewMode = button.dataset.view;
currentViewMode = viewMode;
viewFilterButtons.forEach(function(b) {
b.classList.remove("active-view");
});
button.classList.add("active-view");
renderMemos();
});
});
// --- 初期処理 ---
loadFromStorage();
renderMemos();
</script>
5日目で本当に掴んでほしいこと
- 「ピン」「アーカイブ」「表示モード」のような状態を すべて“ただのプロパティとフラグ”として扱えること
querySelectorAll()+dataset+classListのパターンが、 並び順・タグフィルタ・表示モードフィルタに そのまま再利用できていること- 配列(状態)→ localStorage(保存)→ DOM(描画)→ イベント(操作)という流れの上に、 「重要度」「状態」「フィルタ」「検索」「並び替え」が 自然に積み上がっていること
ここまで来ると、 メモ帳アプリはもう「ただの練習用」ではなく、 自分の生活や仕事で本当に使えるツールになっていきます。 同じパターンを ToDoアプリや家計簿アプリにも どんどん応用してみてください。
メモ帳アプリ 6日目のゴール
6日目のテーマは 「メモ帳アプリを“使いやすく・壊れにくく・安心して使える”状態に仕上げる」 ことです。
1〜5日目で作ったメモ帳アプリは、
- 追加・編集・削除
- 検索・並び替え
- タグ・色ラベル
- ピン留め・アーカイブ
- localStorage に保存
までできる状態でした。
6日目ではここから一歩進めて、
- 入力中のメモを「下書き」として自動保存する
- 文字数カウンターを表示する
- 削除時に確認ダイアログを出す
- 「全メモ削除」ボタンを追加し、安全に初期化できるようにする
- メモ一覧を JSON として「エクスポート/インポート」できるようにする
というところまでを、ステップバイステップで作っていきます。
今日の全体ステップ
6日目の流れはこんな感じです。
- 入力フォームの内容を「下書き」として localStorage に保存する
- ページ読み込み時に「下書き」を復元する
- 入力中の文字数をリアルタイムで表示する
- 削除ボタンに確認ダイアログを追加する
- 「全メモ削除」ボタンを追加し、安全に初期化する
- JSON テキストエリアを使って「エクスポート/インポート」機能を作る
6日目の HTML レイアウト
5日目までの HTML に、 「文字数カウンター」「全削除ボタン」「エクスポート/インポート UI」を追加します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>メモ帳アプリ 6日目</title>
<style>
.memo-item {
border: 1px solid #ccc;
padding: 8px;
margin-bottom: 8px;
}
.memo-item-title {
font-weight: bold;
margin-bottom: 4px;
}
.memo-item-body {
white-space: pre-wrap;
}
.memo-meta {
font-size: 12px;
color: #666;
margin-top: 4px;
}
.editing {
background-color: #fff7e6;
}
.active-sort {
background-color: #007bff;
color: white;
}
.active-tag {
background-color: #28a745;
color: white;
}
.active-view {
background-color: #6f42c1;
color: white;
}
.color-red {
border-left: 4px solid #ff4d4f;
}
.color-blue {
border-left: 4px solid #1890ff;
}
.color-yellow {
border-left: 4px solid #faad14;
}
.color-none {
border-left: 4px solid transparent;
}
.pinned {
border-top: 3px solid #ffc107;
background-color: #fffbe6;
}
.archived {
opacity: 0.6;
}
.counter {
font-size: 12px;
color: #666;
}
.danger {
color: #c00;
font-weight: bold;
}
textarea.export-area {
width: 100%;
height: 120px;
}
</style>
</head>
<body>
<h1>メモ帳アプリ(6日目)</h1>
<div>
<label for="memoTitleInput">タイトル:</label>
<input id="memoTitleInput" type="text" placeholder="メモのタイトルを入力してください">
<span id="titleCounter" class="counter"></span>
</div>
<div>
<label for="memoBodyInput">本文:</label><br>
<textarea id="memoBodyInput" rows="4" cols="40" placeholder="メモの本文を入力してください"></textarea>
<div id="bodyCounter" class="counter"></div>
</div>
<div>
<label for="memoTagInput">タグ:</label>
<input id="memoTagInput" type="text" placeholder="例:仕事 / プライベート / アイデア">
</div>
<div>
<span>色ラベル:</span>
<label>
<input type="radio" name="memoColor" value="none" checked> なし
</label>
<label>
<input type="radio" name="memoColor" value="red"> 赤
</label>
<label>
<input type="radio" name="memoColor" value="blue"> 青
</label>
<label>
<input type="radio" name="memoColor" value="yellow"> 黄
</label>
</div>
<div>
<button id="addMemoButton">メモを追加</button>
<button id="clearAllButton" class="danger">全メモ削除</button>
</div>
<p id="errorMessage" style="color: red;"></p>
<h2>検索</h2>
<div>
<input id="searchInput" type="text" placeholder="タイトル・本文・タグから検索">
</div>
<h2>並び順</h2>
<div id="sortButtons">
<button data-sort="newest" class="active-sort">新しい順</button>
<button data-sort="oldest">古い順</button>
</div>
<h2>タグフィルタ</h2>
<div id="tagFilterButtons">
<button data-tag="" class="active-tag">すべて</button>
<button data-tag="仕事">仕事</button>
<button data-tag="プライベート">プライベート</button>
<button data-tag="アイデア">アイデア</button>
</div>
<h2>表示モード</h2>
<div id="viewFilterButtons">
<button data-view="all" class="active-view">通常(アーカイブ以外)</button>
<button data-view="withArchived">アーカイブも含める</button>
<button data-view="archivedOnly">アーカイブのみ</button>
</div>
<h2>メモ一覧</h2>
<div id="memoList"></div>
<h2>データのエクスポート/インポート</h2>
<p>現在のメモデータを JSON としてコピーしたり、貼り付けて復元したりできます。</p>
<div>
<button id="exportButton">JSONを生成(エクスポート)</button>
<button id="importButton">JSONから復元(インポート)</button>
</div>
<div>
<textarea id="exportArea" class="export-area" placeholder="ここにJSONが表示されます/貼り付けてインポートできます"></textarea>
</div>
<script>
// ここに JavaScript を書いていきます
</script>
</body>
</html>
ここで押さえておきたいポイント
- タイトルと本文の近くに「文字数カウンター」を置く
- 「全メモ削除」ボタンは
.dangerクラスで注意喚起 - JSON エクスポート/インポート用に
#exportAreaテキストエリアと#exportButton/#importButtonを用意する
状態と DOM 参照の定義
6日目では、 「下書き」と「エクスポート/インポート」を扱います。
// --- アプリの状態(State) ---
let memos = [];
let nextMemoId = 1;
let currentKeyword = "";
let currentSort = "newest";
let currentTagFilter = "";
let currentViewMode = "all";
const STORAGE_KEY = "my_memo_app_data";
const DRAFT_STORAGE_KEY = "my_memo_app_draft";
// --- DOM参照 ---
const memoTitleInput = document.querySelector("#memoTitleInput");
const memoBodyInput = document.querySelector("#memoBodyInput");
const memoTagInput = document.querySelector("#memoTagInput");
const addMemoButton = document.querySelector("#addMemoButton");
const clearAllButton = document.querySelector("#clearAllButton");
const errorMessage = document.querySelector("#errorMessage");
const memoList = document.querySelector("#memoList");
const titleCounter = document.querySelector("#titleCounter");
const bodyCounter = document.querySelector("#bodyCounter");
const searchInput = document.querySelector("#searchInput");
const sortButtons = document.querySelectorAll("#sortButtons button");
const tagFilterButtons = document.querySelectorAll("#tagFilterButtons button");
const viewFilterButtons = document.querySelectorAll("#viewFilterButtons button");
const memoColorRadios = document.querySelectorAll("input[name='memoColor']");
const exportButton = document.querySelector("#exportButton");
const importButton = document.querySelector("#importButton");
const exportArea = document.querySelector("#exportArea");
JavaScript深掘りポイント:
- 「下書き」はメモとは別のキー(
DRAFT_STORAGE_KEY)で保存する - エクスポート/インポート用のテキストエリアも
querySelector()で普通に扱える
localStorage の保存・読み込み(メモ本体)
メモ本体の保存・読み込みは 5日目と同じパターンです。
// --- ストレージ:状態をlocalStorageに保存する ---
function saveToStorage() {
const data = {
memos: memos,
nextMemoId: nextMemoId
};
const json = JSON.stringify(data);
localStorage.setItem(STORAGE_KEY, json);
}
// --- ストレージ:localStorageから状態を読み込む ---
function loadFromStorage() {
const json = localStorage.getItem(STORAGE_KEY);
if (json === null) {
return;
}
try {
const data = JSON.parse(json);
if (Array.isArray(data.memos) && typeof data.nextMemoId === "number") {
memos = data.memos;
nextMemoId = data.nextMemoId;
}
} catch (error) {
console.error("保存データの読み込みに失敗しました:", error);
}
}
JavaScript下書きの保存・読み込み
入力中のタイトル・本文・タグ・色ラベルを 「下書き」として localStorage に保存します。
// --- ストレージ:下書きを保存する ---
function saveDraft() {
const draft = {
title: memoTitleInput.value,
body: memoBodyInput.value,
tag: memoTagInput.value,
color: getSelectedColor()
};
const json = JSON.stringify(draft);
localStorage.setItem(DRAFT_STORAGE_KEY, json);
}
// --- ストレージ:下書きを読み込む ---
function loadDraft() {
const json = localStorage.getItem(DRAFT_STORAGE_KEY);
if (json === null) {
return;
}
try {
const draft = JSON.parse(json);
memoTitleInput.value = draft.title || "";
memoBodyInput.value = draft.body || "";
memoTagInput.value = draft.tag || "";
memoColorRadios.forEach(function(radio) {
radio.checked = radio.value === (draft.color || "none");
});
updateCounters();
} catch (error) {
console.error("下書きの読み込みに失敗しました:", error);
}
}
JavaScript深掘りポイント:
- 「下書き」はメモ一覧とは別のオブジェクトとして保存する
- ページを閉じても、次に開いたときに入力途中の内容が復元される
updateCounters()を呼んで、復元後の文字数表示も整える
色ラベルの選択値を取得する関数(復習)
// --- ユーティリティ:選択されている色ラベルを取得する ---
function getSelectedColor() {
let selectedValue = "none";
memoColorRadios.forEach(function(radio) {
if (radio.checked) {
selectedValue = radio.value;
}
});
return selectedValue;
}
JavaScript文字数カウンターの更新
タイトルと本文の文字数をリアルタイムで表示します。
// --- ユーティリティ:文字数カウンターを更新する ---
function updateCounters() {
const titleLength = memoTitleInput.value.length;
const bodyLength = memoBodyInput.value.length;
titleCounter.textContent = `文字数: ${titleLength}`;
bodyCounter.textContent = `本文の文字数: ${bodyLength}`;
}
JavaScriptイベントでカウンターを動かす
memoTitleInput.addEventListener("input", function() {
updateCounters();
saveDraft(); // 入力のたびに下書きを保存
});
memoBodyInput.addEventListener("input", function() {
updateCounters();
saveDraft();
});
memoTagInput.addEventListener("input", function() {
saveDraft();
});
memoColorRadios.forEach(function(radio) {
radio.addEventListener("change", function() {
saveDraft();
});
});
JavaScript深掘りポイント:
- 「入力のたびに下書きを保存する」ことで、 ブラウザを閉じても安心して続きが書ける
- 文字数カウンターは「ただの
value.length」だが、 UI の安心感が一気に上がる
メモ追加ロジック:下書きのクリアも行う
メモ追加後に、 フォームをクリアし、下書きも削除します。
// --- 操作ロジック:メモを追加する ---
function addMemo(title, body, tag, color) {
const newMemo = {
id: nextMemoId,
title: title,
body: body,
tag: tag,
color: color,
pinned: false,
archived: false,
createdAt: Date.now()
};
nextMemoId = nextMemoId + 1;
memos.push(newMemo);
saveToStorage();
}
JavaScriptメモ編集・ピン・アーカイブ・削除ロジック(復習+削除確認)
編集・ピン・アーカイブは 5日目と同じです。 削除だけ「確認ダイアログ」を追加します。
// --- 操作ロジック:メモを編集する ---
function updateMemo(id, newTitle, newBody, newTag, newColor) {
memos.forEach(function(memo) {
if (memo.id === id) {
memo.title = newTitle;
memo.body = newBody;
memo.tag = newTag;
memo.color = newColor;
}
});
saveToStorage();
renderMemos();
}
// --- 操作ロジック:ピン状態切り替え ---
function togglePinned(id) {
memos.forEach(function(memo) {
if (memo.id === id) {
memo.pinned = !memo.pinned;
}
});
saveToStorage();
renderMemos();
}
// --- 操作ロジック:アーカイブ状態切り替え ---
function toggleArchived(id) {
memos.forEach(function(memo) {
if (memo.id === id) {
memo.archived = !memo.archived;
}
});
saveToStorage();
renderMemos();
}
// --- 操作ロジック:メモ削除(確認付き) ---
function deleteMemo(id) {
const ok = window.confirm("このメモを削除しますか?(元に戻せません)");
if (!ok) {
return;
}
const newMemos = memos.filter(function(memo) {
return memo.id !== id;
});
memos.length = 0;
newMemos.forEach(function(memo) {
memos.push(memo);
});
saveToStorage();
renderMemos();
}
JavaScript深掘りポイント:
window.confirm()は「OK/キャンセル」のシンプルな確認ダイアログ- 「元に戻せません」と明示することで、 ユーザーが誤操作をしにくくなる
全メモ削除ロジック
「全メモ削除」ボタンで、 アプリ全体を初期化できるようにします。
// --- 操作ロジック:全メモ削除 ---
function clearAllMemos() {
const ok = window.confirm("本当に全てのメモを削除しますか?(元に戻せません)");
if (!ok) {
return;
}
memos.length = 0;
nextMemoId = 1;
saveToStorage();
renderMemos();
}
JavaScript6日目の心臓部:描画(renderMemos)
描画ロジックは 5日目のものをベースにします。 ここでは「削除ボタンに確認が付いた」ことだけ意識して見てください。
// --- 描画:状態から画面(DOM)を作り直す ---
function renderMemos() {
memoList.innerHTML = "";
let visibleMemos = memos;
if (currentKeyword.trim() !== "") {
const keywordLower = currentKeyword.toLowerCase();
visibleMemos = visibleMemos.filter(function(memo) {
const titleLower = memo.title.toLowerCase();
const bodyLower = memo.body.toLowerCase();
const tagLower = (memo.tag || "").toLowerCase();
return (
titleLower.includes(keywordLower) ||
bodyLower.includes(keywordLower) ||
tagLower.includes(keywordLower)
);
});
}
if (currentTagFilter !== "") {
visibleMemos = visibleMemos.filter(function(memo) {
return memo.tag === currentTagFilter;
});
}
if (currentViewMode === "all") {
visibleMemos = visibleMemos.filter(function(memo) {
return memo.archived === false;
});
} else if (currentViewMode === "archivedOnly") {
visibleMemos = visibleMemos.filter(function(memo) {
return memo.archived === true;
});
}
visibleMemos = visibleMemos.slice();
visibleMemos.sort(function(a, b) {
if (a.pinned && !b.pinned) {
return -1;
}
if (!a.pinned && b.pinned) {
return 1;
}
if (currentSort === "newest") {
return b.createdAt - a.createdAt;
} else {
return a.createdAt - b.createdAt;
}
});
visibleMemos.forEach(function(memo) {
const memoDiv = document.createElement("div");
memoDiv.classList.add("memo-item");
memoDiv.dataset.id = String(memo.id);
const colorClass = "color-" + (memo.color || "none");
memoDiv.classList.add(colorClass);
if (memo.pinned) {
memoDiv.classList.add("pinned");
}
if (memo.archived) {
memoDiv.classList.add("archived");
}
const titleDiv = document.createElement("div");
titleDiv.classList.add("memo-item-title");
titleDiv.textContent = memo.title;
const bodyDiv = document.createElement("div");
bodyDiv.classList.add("memo-item-body");
bodyDiv.textContent = memo.body;
const metaDiv = document.createElement("div");
metaDiv.classList.add("memo-meta");
metaDiv.textContent =
`タグ: ${memo.tag || "(なし)"} / 色: ${memo.color || "none"} / ` +
`ピン: ${memo.pinned ? "ON" : "OFF"} / アーカイブ: ${memo.archived ? "ON" : "OFF"}`;
const editTitleInput = document.createElement("input");
editTitleInput.type = "text";
editTitleInput.value = memo.title;
editTitleInput.style.display = "none";
const editBodyTextarea = document.createElement("textarea");
editBodyTextarea.rows = 4;
editBodyTextarea.cols = 40;
editBodyTextarea.value = memo.body;
editBodyTextarea.style.display = "none";
const editTagInput = document.createElement("input");
editTagInput.type = "text";
editTagInput.value = memo.tag || "";
editTagInput.style.display = "none";
const editColorContainer = document.createElement("div");
editColorContainer.style.display = "none";
const colors = ["none", "red", "blue", "yellow"];
const editColorRadios = [];
colors.forEach(function(color) {
const label = document.createElement("label");
const radio = document.createElement("input");
radio.type = "radio";
radio.name = "editColor-" + memo.id;
radio.value = color;
if (memo.color === color || (!memo.color && color === "none")) {
radio.checked = true;
}
label.append(radio);
label.append(document.createTextNode(" " + color));
editColorContainer.append(label);
editColorRadios.push(radio);
});
const editButton = document.createElement("button");
editButton.textContent = "編集";
const saveButton = document.createElement("button");
saveButton.textContent = "保存";
saveButton.style.display = "none";
const cancelButton = document.createElement("button");
cancelButton.textContent = "キャンセル";
cancelButton.style.display = "none";
const deleteButton = document.createElement("button");
deleteButton.textContent = "削除";
const pinButton = document.createElement("button");
pinButton.textContent = memo.pinned ? "ピン解除" : "ピン";
const archiveButton = document.createElement("button");
archiveButton.textContent = memo.archived ? "アーカイブ解除" : "アーカイブ";
editButton.addEventListener("click", function() {
titleDiv.style.display = "none";
bodyDiv.style.display = "none";
metaDiv.style.display = "none";
editTitleInput.style.display = "block";
editBodyTextarea.style.display = "block";
editTagInput.style.display = "block";
editColorContainer.style.display = "block";
editButton.style.display = "none";
deleteButton.style.display = "none";
pinButton.style.display = "none";
archiveButton.style.display = "none";
saveButton.style.display = "inline-block";
cancelButton.style.display = "inline-block";
memoDiv.classList.add("editing");
editTitleInput.value = memo.title;
editBodyTextarea.value = memo.body;
editTagInput.value = memo.tag || "";
editTitleInput.focus();
});
saveButton.addEventListener("click", function() {
const newTitle = editTitleInput.value;
const newBody = editBodyTextarea.value;
const newTag = editTagInput.value;
if (newTitle.trim() === "" && newBody.trim() === "") {
alert("タイトルか本文のどちらかは入力してください。");
return;
}
let newColor = "none";
editColorRadios.forEach(function(radio) {
if (radio.checked) {
newColor = radio.value;
}
});
const idText = memoDiv.dataset.id;
const id = Number(idText);
updateMemo(id, newTitle, newBody, newTag, newColor);
});
cancelButton.addEventListener("click", function() {
titleDiv.style.display = "block";
bodyDiv.style.display = "block";
metaDiv.style.display = "block";
editTitleInput.style.display = "none";
editBodyTextarea.style.display = "none";
editTagInput.style.display = "none";
editColorContainer.style.display = "none";
editButton.style.display = "inline-block";
deleteButton.style.display = "inline-block";
pinButton.style.display = "inline-block";
archiveButton.style.display = "inline-block";
saveButton.style.display = "none";
cancelButton.style.display = "none";
memoDiv.classList.remove("editing");
});
deleteButton.addEventListener("click", function(event) {
const memoElement = event.target.closest(".memo-item");
const idText = memoElement.dataset.id;
const id = Number(idText);
deleteMemo(id);
});
pinButton.addEventListener("click", function(event) {
const memoElement = event.target.closest(".memo-item");
const idText = memoElement.dataset.id;
const id = Number(idText);
togglePinned(id);
});
archiveButton.addEventListener("click", function(event) {
const memoElement = event.target.closest(".memo-item");
const idText = memoElement.dataset.id;
const id = Number(idText);
toggleArchived(id);
});
memoDiv.append(titleDiv);
memoDiv.append(bodyDiv);
memoDiv.append(metaDiv);
memoDiv.append(editTitleInput);
memoDiv.append(editBodyTextarea);
memoDiv.append(editTagInput);
memoDiv.append(editColorContainer);
memoDiv.append(editButton);
memoDiv.append(saveButton);
memoDiv.append(cancelButton);
memoDiv.append(deleteButton);
memoDiv.append(pinButton);
memoDiv.append(archiveButton);
memoList.append(memoDiv);
});
}
JavaScriptエクスポート機能:JSONを生成してテキストエリアに表示
メモ一覧を JSON にして、 テキストエリアに表示します。
// --- 操作ロジック:エクスポート(JSON生成) ---
function exportData() {
const data = {
memos: memos,
nextMemoId: nextMemoId
};
const json = JSON.stringify(data, null, 2); // 見やすいように整形
exportArea.value = json;
}
JavaScriptインポート機能:JSONから復元
テキストエリアに貼り付けた JSON から、 メモ一覧を復元します。
// --- 操作ロジック:インポート(JSONから復元) ---
function importData() {
const text = exportArea.value;
if (text.trim() === "") {
alert("JSONが空です。エクスポートしたデータを貼り付けてください。");
return;
}
try {
const data = JSON.parse(text);
if (!Array.isArray(data.memos) || typeof data.nextMemoId !== "number") {
alert("JSONの形式が正しくありません。");
return;
}
const ok = window.confirm("現在のメモを上書きして復元しますか?");
if (!ok) {
return;
}
memos.length = 0;
data.memos.forEach(function(memo) {
memos.push(memo);
});
nextMemoId = data.nextMemoId;
saveToStorage();
renderMemos();
alert("復元が完了しました。");
} catch (error) {
console.error("インポートに失敗しました:", error);
alert("JSONの解析に失敗しました。形式を確認してください。");
}
}
JavaScript深掘りポイント:
JSON.stringify()とJSON.parse()を使うことで、 「アプリの状態を丸ごとテキストに変換/復元」できる- インポート時には必ず確認ダイアログを挟み、 誤操作でデータを消さないようにする
イベント設定まとめ
最後に、各ボタン・入力欄にイベントを設定します。
// --- メモ追加ボタン ---
addMemoButton.addEventListener("click", function() {
const title = memoTitleInput.value;
const body = memoBodyInput.value;
const tag = memoTagInput.value;
const color = getSelectedColor();
errorMessage.textContent = "";
if (title.trim() === "" && body.trim() === "") {
errorMessage.textContent = "タイトルか本文のどちらかは入力してください。";
return;
}
addMemo(title, body, tag, color);
memoTitleInput.value = "";
memoBodyInput.value = "";
memoTagInput.value = "";
memoColorRadios.forEach(function(radio) {
radio.checked = radio.value === "none";
});
localStorage.removeItem(DRAFT_STORAGE_KEY); // 下書きを消す
updateCounters();
renderMemos();
});
// --- 全メモ削除ボタン ---
clearAllButton.addEventListener("click", function() {
clearAllMemos();
});
// --- 検索入力欄 ---
searchInput.addEventListener("input", function() {
currentKeyword = searchInput.value;
renderMemos();
});
// --- 並び順ボタン ---
sortButtons.forEach(function(button) {
button.addEventListener("click", function() {
const sortMode = button.dataset.sort;
currentSort = sortMode;
sortButtons.forEach(function(b) {
b.classList.remove("active-sort");
});
button.classList.add("active-sort");
renderMemos();
});
});
// --- タグフィルタボタン ---
tagFilterButtons.forEach(function(button) {
button.addEventListener("click", function() {
const tag = button.dataset.tag;
currentTagFilter = tag;
tagFilterButtons.forEach(function(b) {
b.classList.remove("active-tag");
});
button.classList.add("active-tag");
renderMemos();
});
});
// --- 表示モードフィルタボタン ---
viewFilterButtons.forEach(function(button) {
button.addEventListener("click", function() {
const viewMode = button.dataset.view;
currentViewMode = viewMode;
viewFilterButtons.forEach(function(b) {
b.classList.remove("active-view");
});
button.classList.add("active-view");
renderMemos();
});
});
// --- エクスポートボタン ---
exportButton.addEventListener("click", function() {
exportData();
});
// --- インポートボタン ---
importButton.addEventListener("click", function() {
importData();
});
JavaScript初期処理
ページ読み込み時に、 保存データと下書きを読み込み、カウンターを更新します。
// --- 初期処理 ---
loadFromStorage();
loadDraft();
updateCounters();
renderMemos();
JavaScript6日目で本当に掴んでほしいこと
- 「下書き」「文字数カウンター」「削除確認」「全削除」「エクスポート/インポート」は すべて“状態+localStorage+DOM+イベント”の組み合わせで作れる
querySelector()/querySelectorAll()/addEventListener()/createElement()/append()/classList/datasetのパターンが、 ここまで来ると完全に「自分の道具」になっているはず- アプリは「機能が増える」だけでなく、 安心して使えるようにする工夫(確認・下書き・バックアップ)が とても大事だということ
この6日目までのメモ帳アプリは、 もう立派に「自分の生活や仕事で使えるプロダクト」です。 同じ考え方を ToDoアプリや家計簿アプリにも どんどん応用して、自分だけのツール群を育てていってください。
メモ帳アプリ 7日目のゴール
7日目のテーマは 「7日間で育ててきたメモ帳アプリを“完成版”としてまとめ上げる」 ことです。
1〜6日目で作ったメモ帳アプリはすでに、
- 追加・編集・削除
- 検索・並び替え
- タグ・色ラベル
- ピン留め・アーカイブ
- 下書き保存・文字数カウンター
- 全削除・エクスポート/インポート
- localStorage による永続化
まで備えた、かなり実用的なツールになっています。
7日目ではここから仕上げとして、
- コードを「読みやすく・直しやすく」整理する
- よく使う処理を関数にまとめて再利用する
- UI のちょっとした改善(キーボード操作・フォーカス)を入れる
- 「アプリの構造」を意識して、全体を見渡せるようにする
というところまでを、ステップバイステップでやっていきます。
今日の全体ステップ
7日目の流れはこんな感じです。
- アプリ全体の構造を「役割ごと」に整理して眺める
- よく出てくる処理を小さな関数に切り出す
- イベント設定を「まとまったブロック」として整理する
- キーボード操作(Enterで追加、Escで編集キャンセル)を追加する
- フォーカスの動き(追加後にタイトルへ戻す)を整える
- 完成版の HTML+JavaScript をひとつのテンプレートとしてまとめる
アプリの構造をざっくり整理する
まず、メモ帳アプリのコードを「役割ごと」に分けて考えます。
- 状態(State)
memos(メモ一覧)nextMemoId(次に使うID)currentKeyword(検索キーワード)currentSort(並び順)currentTagFilter(タグフィルタ)currentViewMode(表示モード)
- DOM参照
- 入力フォーム(タイトル・本文・タグ・色)
- ボタン類(追加・全削除・並び順・フィルタ・エクスポート・インポート)
- メモ一覧表示領域
- 文字数カウンター
- JSONテキストエリア
- ストレージ関連
- メモ本体の保存・読み込み(
STORAGE_KEY) - 下書きの保存・読み込み(
DRAFT_STORAGE_KEY)
- メモ本体の保存・読み込み(
- ユーティリティ関数
- 色ラベル取得(
getSelectedColor()) - 文字数カウンター更新(
updateCounters()) - 下書き保存・読み込み(
saveDraft()/loadDraft())
- 色ラベル取得(
- 操作ロジック
- メモ追加・編集・削除
- ピン状態切り替え・アーカイブ状態切り替え
- 全メモ削除
- エクスポート・インポート
- 描画ロジック
renderMemos():状態から DOM を作り直す
- イベント設定
- 入力欄(タイトル・本文・タグ・色)
- ボタン(追加・全削除・並び順・タグフィルタ・表示モード・エクスポート・インポート)
この「役割ごとの整理」ができると、 コードを読むとき・直すときに「どこを触ればいいか」が 一気に分かりやすくなります。
よく出てくる処理を小さな関数に切り出す
7日目では、 「同じような処理が何度も出てきているところ」を 小さな関数にまとめていきます。
例1:ボタンの「アクティブ状態」を切り替える処理
並び順ボタン・タグフィルタボタン・表示モードボタンは、 どれも同じパターンで「選択中のボタンにクラスを付ける」 処理を書いていました。
これをひとつの関数にまとめます。
// --- ユーティリティ:ボタン群のアクティブ状態を切り替える ---
function setActiveButton(buttons, activeButton, activeClassName) {
buttons.forEach(function(button) {
button.classList.remove(activeClassName);
});
activeButton.classList.add(activeClassName);
}
JavaScriptこれを使うと、並び順ボタンのイベントはこう書けます。
sortButtons.forEach(function(button) {
button.addEventListener("click", function() {
const sortMode = button.dataset.sort;
currentSort = sortMode;
setActiveButton(sortButtons, button, "active-sort");
renderMemos();
});
});
JavaScriptタグフィルタボタンも同じように書けます。
tagFilterButtons.forEach(function(button) {
button.addEventListener("click", function() {
const tag = button.dataset.tag;
currentTagFilter = tag;
setActiveButton(tagFilterButtons, button, "active-tag");
renderMemos();
});
});
JavaScript表示モードボタンも同じです。
viewFilterButtons.forEach(function(button) {
button.addEventListener("click", function() {
const viewMode = button.dataset.view;
currentViewMode = viewMode;
setActiveButton(viewFilterButtons, button, "active-view");
renderMemos();
});
});
JavaScript深掘りポイント:
- 「同じパターンが3回出てきたら、関数にまとめる」
setActiveButton()のような小さな関数が増えるほど、 コードは「読みやすく・直しやすく」なっていく
キーボード操作を追加する
アプリを少しだけ「気持ちよく」使えるように、 キーボード操作を追加します。
Enterキーでメモ追加
タイトル入力欄で Enter を押したら、 「メモを追加」ボタンを押したのと同じ動きをさせます。
memoTitleInput.addEventListener("keydown", function(event) {
if (event.key === "Enter") {
event.preventDefault(); // フォームの送信などを防ぐ
addMemoFromForm();
}
});
JavaScriptここで、 「フォームの内容からメモを追加する処理」を addMemoFromForm() という関数にまとめておきます。
// --- ユーティリティ:フォームからメモを追加する ---
function addMemoFromForm() {
const title = memoTitleInput.value;
const body = memoBodyInput.value;
const tag = memoTagInput.value;
const color = getSelectedColor();
errorMessage.textContent = "";
if (title.trim() === "" && body.trim() === "") {
errorMessage.textContent = "タイトルか本文のどちらかは入力してください。";
return;
}
addMemo(title, body, tag, color);
memoTitleInput.value = "";
memoBodyInput.value = "";
memoTagInput.value = "";
memoColorRadios.forEach(function(radio) {
radio.checked = radio.value === "none";
});
localStorage.removeItem(DRAFT_STORAGE_KEY);
updateCounters();
memoTitleInput.focus(); // 追加後にタイトル入力欄へフォーカスを戻す
renderMemos();
}
JavaScript「メモを追加」ボタンのイベントも、 この関数を呼ぶだけにします。
addMemoButton.addEventListener("click", function() {
addMemoFromForm();
});
JavaScriptEscキーで編集キャンセル
編集モードのときに Esc を押したら、 「キャンセル」ボタンを押したのと同じ動きをさせます。
これは renderMemos() の中で、 編集用の要素を作るときに設定します。
cancelButton.addEventListener("click", function() {
exitEditMode();
});
editTitleInput.addEventListener("keydown", function(event) {
if (event.key === "Escape") {
exitEditMode();
}
});
editBodyTextarea.addEventListener("keydown", function(event) {
if (event.key === "Escape") {
exitEditMode();
}
});
JavaScriptここで exitEditMode() という関数を そのメモの編集状態を抜ける処理としてまとめます。
function exitEditMode() {
titleDiv.style.display = "block";
bodyDiv.style.display = "block";
metaDiv.style.display = "block";
editTitleInput.style.display = "none";
editBodyTextarea.style.display = "none";
editTagInput.style.display = "none";
editColorContainer.style.display = "none";
editButton.style.display = "inline-block";
deleteButton.style.display = "inline-block";
pinButton.style.display = "inline-block";
archiveButton.style.display = "inline-block";
saveButton.style.display = "none";
cancelButton.style.display = "none";
memoDiv.classList.remove("editing");
}
JavaScript深掘りポイント:
- 「ボタンとキーボードで同じ動きをさせる」ことで、 アプリがぐっと使いやすくなる
- キーボード操作は
keydownイベントで拾い、event.keyを見て分岐する
完成版テンプレート:HTML+JavaScript
ここまでの内容を踏まえて、 7日目の「完成版メモ帳アプリ」を ひとつのテンプレートとしてまとめます。
完成版 HTML
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>メモ帳アプリ 完成版</title>
<style>
.memo-item {
border: 1px solid #ccc;
padding: 8px;
margin-bottom: 8px;
}
.memo-item-title {
font-weight: bold;
margin-bottom: 4px;
}
.memo-item-body {
white-space: pre-wrap;
}
.memo-meta {
font-size: 12px;
color: #666;
margin-top: 4px;
}
.editing {
background-color: #fff7e6;
}
.active-sort {
background-color: #007bff;
color: white;
}
.active-tag {
background-color: #28a745;
color: white;
}
.active-view {
background-color: #6f42c1;
color: white;
}
.color-red {
border-left: 4px solid #ff4d4f;
}
.color-blue {
border-left: 4px solid #1890ff;
}
.color-yellow {
border-left: 4px solid #faad14;
}
.color-none {
border-left: 4px solid transparent;
}
.pinned {
border-top: 3px solid #ffc107;
background-color: #fffbe6;
}
.archived {
opacity: 0.6;
}
.counter {
font-size: 12px;
color: #666;
margin-left: 8px;
}
.danger {
color: #c00;
font-weight: bold;
}
textarea.export-area {
width: 100%;
height: 120px;
}
</style>
</head>
<body>
<h1>メモ帳アプリ(7日間完成版)</h1>
<div>
<label for="memoTitleInput">タイトル:</label>
<input id="memoTitleInput" type="text" placeholder="メモのタイトルを入力してください">
<span id="titleCounter" class="counter"></span>
</div>
<div>
<label for="memoBodyInput">本文:</label><br>
<textarea id="memoBodyInput" rows="4" cols="40" placeholder="メモの本文を入力してください"></textarea>
<div id="bodyCounter" class="counter"></div>
</div>
<div>
<label for="memoTagInput">タグ:</label>
<input id="memoTagInput" type="text" placeholder="例:仕事 / プライベート / アイデア">
</div>
<div>
<span>色ラベル:</span>
<label>
<input type="radio" name="memoColor" value="none" checked> なし
</label>
<label>
<input type="radio" name="memoColor" value="red"> 赤
</label>
<label>
<input type="radio" name="memoColor" value="blue"> 青
</label>
<label>
<input type="radio" name="memoColor" value="yellow"> 黄
</label>
</div>
<div>
<button id="addMemoButton">メモを追加(Enterでも追加)</button>
<button id="clearAllButton" class="danger">全メモ削除</button>
</div>
<p id="errorMessage" style="color: red;"></p>
<h2>検索</h2>
<div>
<input id="searchInput" type="text" placeholder="タイトル・本文・タグから検索">
</div>
<h2>並び順</h2>
<div id="sortButtons">
<button data-sort="newest" class="active-sort">新しい順</button>
<button data-sort="oldest">古い順</button>
</div>
<h2>タグフィルタ</h2>
<div id="tagFilterButtons">
<button data-tag="" class="active-tag">すべて</button>
<button data-tag="仕事">仕事</button>
<button data-tag="プライベート">プライベート</button>
<button data-tag="アイデア">アイデア</button>
</div>
<h2>表示モード</h2>
<div id="viewFilterButtons">
<button data-view="all" class="active-view">通常(アーカイブ以外)</button>
<button data-view="withArchived">アーカイブも含める</button>
<button data-view="archivedOnly">アーカイブのみ</button>
</div>
<h2>メモ一覧</h2>
<div id="memoList"></div>
<h2>データのエクスポート/インポート</h2>
<p>現在のメモデータを JSON としてコピーしたり、貼り付けて復元したりできます。</p>
<div>
<button id="exportButton">JSONを生成(エクスポート)</button>
<button id="importButton">JSONから復元(インポート)</button>
</div>
<div>
<textarea id="exportArea" class="export-area" placeholder="ここにJSONが表示されます/貼り付けてインポートできます"></textarea>
</div>
<script>
// --- アプリの状態(State) ---
let memos = [];
let nextMemoId = 1;
let currentKeyword = "";
let currentSort = "newest";
let currentTagFilter = "";
let currentViewMode = "all";
const STORAGE_KEY = "my_memo_app_data";
const DRAFT_STORAGE_KEY = "my_memo_app_draft";
// --- DOM参照 ---
const memoTitleInput = document.querySelector("#memoTitleInput");
const memoBodyInput = document.querySelector("#memoBodyInput");
const memoTagInput = document.querySelector("#memoTagInput");
const addMemoButton = document.querySelector("#addMemoButton");
const clearAllButton = document.querySelector("#clearAllButton");
const errorMessage = document.querySelector("#errorMessage");
const memoList = document.querySelector("#memoList");
const titleCounter = document.querySelector("#titleCounter");
const bodyCounter = document.querySelector("#bodyCounter");
const searchInput = document.querySelector("#searchInput");
const sortButtons = document.querySelectorAll("#sortButtons button");
const tagFilterButtons = document.querySelectorAll("#tagFilterButtons button");
const viewFilterButtons = document.querySelectorAll("#viewFilterButtons button");
const memoColorRadios = document.querySelectorAll("input[name='memoColor']");
const exportButton = document.querySelector("#exportButton");
const importButton = document.querySelector("#importButton");
const exportArea = document.querySelector("#exportArea");
// --- ストレージ:保存 ---
function saveToStorage() {
const data = {
memos: memos,
nextMemoId: nextMemoId
};
const json = JSON.stringify(data);
localStorage.setItem(STORAGE_KEY, json);
}
// --- ストレージ:読み込み ---
function loadFromStorage() {
const json = localStorage.getItem(STORAGE_KEY);
if (json === null) {
return;
}
try {
const data = JSON.parse(json);
if (Array.isArray(data.memos) && typeof data.nextMemoId === "number") {
memos = data.memos;
nextMemoId = data.nextMemoId;
}
} catch (error) {
console.error("保存データの読み込みに失敗しました:", error);
}
}
// --- ストレージ:下書き保存 ---
function saveDraft() {
const draft = {
title: memoTitleInput.value,
body: memoBodyInput.value,
tag: memoTagInput.value,
color: getSelectedColor()
};
const json = JSON.stringify(draft);
localStorage.setItem(DRAFT_STORAGE_KEY, json);
}
// --- ストレージ:下書き読み込み ---
function loadDraft() {
const json = localStorage.getItem(DRAFT_STORAGE_KEY);
if (json === null) {
return;
}
try {
const draft = JSON.parse(json);
memoTitleInput.value = draft.title || "";
memoBodyInput.value = draft.body || "";
memoTagInput.value = draft.tag || "";
memoColorRadios.forEach(function(radio) {
radio.checked = radio.value === (draft.color || "none");
});
updateCounters();
} catch (error) {
console.error("下書きの読み込みに失敗しました:", error);
}
}
// --- ユーティリティ:選択されている色ラベルを取得 ---
function getSelectedColor() {
let selectedValue = "none";
memoColorRadios.forEach(function(radio) {
if (radio.checked) {
selectedValue = radio.value;
}
});
return selectedValue;
}
// --- ユーティリティ:文字数カウンター更新 ---
function updateCounters() {
const titleLength = memoTitleInput.value.length;
const bodyLength = memoBodyInput.value.length;
titleCounter.textContent = `文字数: ${titleLength}`;
bodyCounter.textContent = `本文の文字数: ${bodyLength}`;
}
// --- ユーティリティ:ボタン群のアクティブ状態を切り替える ---
function setActiveButton(buttons, activeButton, activeClassName) {
buttons.forEach(function(button) {
button.classList.remove(activeClassName);
});
activeButton.classList.add(activeClassName);
}
// --- 操作ロジック:メモ追加 ---
function addMemo(title, body, tag, color) {
const newMemo = {
id: nextMemoId,
title: title,
body: body,
tag: tag,
color: color,
pinned: false,
archived: false,
createdAt: Date.now()
};
nextMemoId = nextMemoId + 1;
memos.push(newMemo);
saveToStorage();
}
// --- 操作ロジック:メモ編集 ---
function updateMemo(id, newTitle, newBody, newTag, newColor) {
memos.forEach(function(memo) {
if (memo.id === id) {
memo.title = newTitle;
memo.body = newBody;
memo.tag = newTag;
memo.color = newColor;
}
});
saveToStorage();
renderMemos();
}
// --- 操作ロジック:ピン状態切り替え ---
function togglePinned(id) {
memos.forEach(function(memo) {
if (memo.id === id) {
memo.pinned = !memo.pinned;
}
});
saveToStorage();
renderMemos();
}
// --- 操作ロジック:アーカイブ状態切り替え ---
function toggleArchived(id) {
memos.forEach(function(memo) {
if (memo.id === id) {
memo.archived = !memo.archived;
}
});
saveToStorage();
renderMemos();
}
// --- 操作ロジック:メモ削除(確認付き) ---
function deleteMemo(id) {
const ok = window.confirm("このメモを削除しますか?(元に戻せません)");
if (!ok) {
return;
}
const newMemos = memos.filter(function(memo) {
return memo.id !== id;
});
memos.length = 0;
newMemos.forEach(function(memo) {
memos.push(memo);
});
saveToStorage();
renderMemos();
}
// --- 操作ロジック:全メモ削除 ---
function clearAllMemos() {
const ok = window.confirm("本当に全てのメモを削除しますか?(元に戻せません)");
if (!ok) {
return;
}
memos.length = 0;
nextMemoId = 1;
saveToStorage();
renderMemos();
}
// --- 操作ロジック:エクスポート ---
function exportData() {
const data = {
memos: memos,
nextMemoId: nextMemoId
};
const json = JSON.stringify(data, null, 2);
exportArea.value = json;
}
// --- 操作ロジック:インポート ---
function importData() {
const text = exportArea.value;
if (text.trim() === "") {
alert("JSONが空です。エクスポートしたデータを貼り付けてください。");
return;
}
try {
const data = JSON.parse(text);
if (!Array.isArray(data.memos) || typeof data.nextMemoId !== "number") {
alert("JSONの形式が正しくありません。");
return;
}
const ok = window.confirm("現在のメモを上書きして復元しますか?");
if (!ok) {
return;
}
memos.length = 0;
data.memos.forEach(function(memo) {
memos.push(memo);
});
nextMemoId = data.nextMemoId;
saveToStorage();
renderMemos();
alert("復元が完了しました。");
} catch (error) {
console.error("インポートに失敗しました:", error);
alert("JSONの解析に失敗しました。形式を確認してください。");
}
}
// --- ユーティリティ:フォームからメモを追加する ---
function addMemoFromForm() {
const title = memoTitleInput.value;
const body = memoBodyInput.value;
const tag = memoTagInput.value;
const color = getSelectedColor();
errorMessage.textContent = "";
if (title.trim() === "" && body.trim() === "") {
errorMessage.textContent = "タイトルか本文のどちらかは入力してください。";
return;
}
addMemo(title, body, tag, color);
memoTitleInput.value = "";
memoBodyInput.value = "";
memoTagInput.value = "";
memoColorRadios.forEach(function(radio) {
radio.checked = radio.value === "none";
});
localStorage.removeItem(DRAFT_STORAGE_KEY);
updateCounters();
memoTitleInput.focus();
renderMemos();
}
// --- 描画 ---
function renderMemos() {
memoList.innerHTML = "";
let visibleMemos = memos;
if (currentKeyword.trim() !== "") {
const keywordLower = currentKeyword.toLowerCase();
visibleMemos = visibleMemos.filter(function(memo) {
const titleLower = memo.title.toLowerCase();
const bodyLower = memo.body.toLowerCase();
const tagLower = (memo.tag || "").toLowerCase();
return (
titleLower.includes(keywordLower) ||
bodyLower.includes(keywordLower) ||
tagLower.includes(keywordLower)
);
});
}
if (currentTagFilter !== "") {
visibleMemos = visibleMemos.filter(function(memo) {
return memo.tag === currentTagFilter;
});
}
if (currentViewMode === "all") {
visibleMemos = visibleMemos.filter(function(memo) {
return memo.archived === false;
});
} else if (currentViewMode === "archivedOnly") {
visibleMemos = visibleMemos.filter(function(memo) {
return memo.archived === true;
});
}
visibleMemos = visibleMemos.slice();
visibleMemos.sort(function(a, b) {
if (a.pinned && !b.pinned) {
return -1;
}
if (!a.pinned && b.pinned) {
return 1;
}
if (currentSort === "newest") {
return b.createdAt - a.createdAt;
} else {
return a.createdAt - b.createdAt;
}
});
visibleMemos.forEach(function(memo) {
const memoDiv = document.createElement("div");
memoDiv.classList.add("memo-item");
memoDiv.dataset.id = String(memo.id);
const colorClass = "color-" + (memo.color || "none");
memoDiv.classList.add(colorClass);
if (memo.pinned) {
memoDiv.classList.add("pinned");
}
if (memo.archived) {
memoDiv.classList.add("archived");
}
const titleDiv = document.createElement("div");
titleDiv.classList.add("memo-item-title");
titleDiv.textContent = memo.title;
const bodyDiv = document.createElement("div");
bodyDiv.classList.add("memo-item-body");
bodyDiv.textContent = memo.body;
const metaDiv = document.createElement("div");
metaDiv.classList.add("memo-meta");
metaDiv.textContent =
`タグ: ${memo.tag || "(なし)"} / 色: ${memo.color || "none"} / ` +
`ピン: ${memo.pinned ? "ON" : "OFF"} / アーカイブ: ${memo.archived ? "ON" : "OFF"}`;
const editTitleInput = document.createElement("input");
editTitleInput.type = "text";
editTitleInput.value = memo.title;
editTitleInput.style.display = "none";
const editBodyTextarea = document.createElement("textarea");
editBodyTextarea.rows = 4;
editBodyTextarea.cols = 40;
editBodyTextarea.value = memo.body;
editBodyTextarea.style.display = "none";
const editTagInput = document.createElement("input");
editTagInput.type = "text";
editTagInput.value = memo.tag || "";
editTagInput.style.display = "none";
const editColorContainer = document.createElement("div");
editColorContainer.style.display = "none";
const colors = ["none", "red", "blue", "yellow"];
const editColorRadios = [];
colors.forEach(function(color) {
const label = document.createElement("label");
const radio = document.createElement("input");
radio.type = "radio";
radio.name = "editColor-" + memo.id;
radio.value = color;
if (memo.color === color || (!memo.color && color === "none")) {
radio.checked = true;
}
label.append(radio);
label.append(document.createTextNode(" " + color));
editColorContainer.append(label);
editColorRadios.push(radio);
});
const editButton = document.createElement("button");
editButton.textContent = "編集";
const saveButton = document.createElement("button");
saveButton.textContent = "保存";
saveButton.style.display = "none";
const cancelButton = document.createElement("button");
cancelButton.textContent = "キャンセル";
cancelButton.style.display = "none";
const deleteButton = document.createElement("button");
deleteButton.textContent = "削除";
const pinButton = document.createElement("button");
pinButton.textContent = memo.pinned ? "ピン解除" : "ピン";
const archiveButton = document.createElement("button");
archiveButton.textContent = memo.archived ? "アーカイブ解除" : "アーカイブ";
function exitEditMode() {
titleDiv.style.display = "block";
bodyDiv.style.display = "block";
metaDiv.style.display = "block";
editTitleInput.style.display = "none";
editBodyTextarea.style.display = "none";
editTagInput.style.display = "none";
editColorContainer.style.display = "none";
editButton.style.display = "inline-block";
deleteButton.style.display = "inline-block";
pinButton.style.display = "inline-block";
archiveButton.style.display = "inline-block";
saveButton.style.display = "none";
cancelButton.style.display = "none";
memoDiv.classList.remove("editing");
}
editButton.addEventListener("click", function() {
titleDiv.style.display = "none";
bodyDiv.style.display = "none";
metaDiv.style.display = "none";
editTitleInput.style.display = "block";
editBodyTextarea.style.display = "block";
editTagInput.style.display = "block";
editColorContainer.style.display = "block";
editButton.style.display = "none";
deleteButton.style.display = "none";
pinButton.style.display = "none";
archiveButton.style.display = "none";
saveButton.style.display = "inline-block";
cancelButton.style.display = "inline-block";
memoDiv.classList.add("editing");
editTitleInput.value = memo.title;
editBodyTextarea.value = memo.body;
editTagInput.value = memo.tag || "";
editTitleInput.focus();
});
saveButton.addEventListener("click", function() {
const newTitle = editTitleInput.value;
const newBody = editBodyTextarea.value;
const newTag = editTagInput.value;
if (newTitle.trim() === "" && newBody.trim() === "") {
alert("タイトルか本文のどちらかは入力してください。");
return;
}
let newColor = "none";
editColorRadios.forEach(function(radio) {
if (radio.checked) {
newColor = radio.value;
}
});
const idText = memoDiv.dataset.id;
const id = Number(idText);
updateMemo(id, newTitle, newBody, newTag, newColor);
});
cancelButton.addEventListener("click", function() {
exitEditMode();
});
editTitleInput.addEventListener("keydown", function(event) {
if (event.key === "Escape") {
exitEditMode();
}
});
editBodyTextarea.addEventListener("keydown", function(event) {
if (event.key === "Escape") {
exitEditMode();
}
});
deleteButton.addEventListener("click", function(event) {
const memoElement = event.target.closest(".memo-item");
const idText = memoElement.dataset.id;
const id = Number(idText);
deleteMemo(id);
});
pinButton.addEventListener("click", function(event) {
const memoElement = event.target.closest(".memo-item");
const idText = memoElement.dataset.id;
const id = Number(idText);
togglePinned(id);
});
archiveButton.addEventListener("click", function(event) {
const memoElement = event.target.closest(".memo-item");
const idText = memoElement.dataset.id;
const id = Number(idText);
toggleArchived(id);
});
memoDiv.append(titleDiv);
memoDiv.append(bodyDiv);
memoDiv.append(metaDiv);
memoDiv.append(editTitleInput);
memoDiv.append(editBodyTextarea);
memoDiv.append(editTagInput);
memoDiv.append(editColorContainer);
memoDiv.append(editButton);
memoDiv.append(saveButton);
memoDiv.append(cancelButton);
memoDiv.append(deleteButton);
memoDiv.append(pinButton);
memoDiv.append(archiveButton);
memoList.append(memoDiv);
});
}
// --- イベント設定:入力欄と下書き ---
memoTitleInput.addEventListener("input", function() {
updateCounters();
saveDraft();
});
memoBodyInput.addEventListener("input", function() {
updateCounters();
saveDraft();
});
memoTagInput.addEventListener("input", function() {
saveDraft();
});
memoColorRadios.forEach(function(radio) {
radio.addEventListener("change", function() {
saveDraft();
});
});
memoTitleInput.addEventListener("keydown", function(event) {
if (event.key === "Enter") {
event.preventDefault();
addMemoFromForm();
}
});
// --- イベント設定:メモ追加ボタン ---
addMemoButton.addEventListener("click", function() {
addMemoFromForm();
});
// --- イベント設定:全メモ削除ボタン ---
clearAllButton.addEventListener("click", function() {
clearAllMemos();
});
// --- イベント設定:検索入力欄 ---
searchInput.addEventListener("input", function() {
currentKeyword = searchInput.value;
renderMemos();
});
// --- イベント設定:並び順ボタン ---
sortButtons.forEach(function(button) {
button.addEventListener("click", function() {
const sortMode = button.dataset.sort;
currentSort = sortMode;
setActiveButton(sortButtons, button, "active-sort");
renderMemos();
});
});
// --- イベント設定:タグフィルタボタン ---
tagFilterButtons.forEach(function(button) {
button.addEventListener("click", function() {
const tag = button.dataset.tag;
currentTagFilter = tag;
setActiveButton(tagFilterButtons, button, "active-tag");
renderMemos();
});
});
// --- イベント設定:表示モードフィルタボタン ---
viewFilterButtons.forEach(function(button) {
button.addEventListener("click", function() {
const viewMode = button.dataset.view;
currentViewMode = viewMode;
setActiveButton(viewFilterButtons, button, "active-view");
renderMemos();
});
});
// --- イベント設定:エクスポート/インポート ---
exportButton.addEventListener("click", function() {
exportData();
});
importButton.addEventListener("click", function() {
importData();
});
// --- 初期処理 ---
loadFromStorage();
loadDraft();
updateCounters();
renderMemos();
memoTitleInput.focus();
</script>
</body>
</html>
7日目で本当に掴んでほしいこと
- アプリは「機能を増やす」だけでなく、「構造を整理する」ことで完成度が上がる
querySelector()/querySelectorAll()/addEventListener()/createElement()/append()/classList/datasetの組み合わせで、 ここまでのメモ帳アプリ全体が成り立っていること- 小さなユーティリティ関数(
setActiveButton()やaddMemoFromForm())を作ることで、 コードが読みやすく・直しやすく・育てやすくなること
この7日間のメモ帳アプリは、 そのまま「自分の生活で使えるプロダクト」として動かせますし、 ToDoアプリ・家計簿アプリ・習慣トラッカーなど、 他のアプリを作るときの強力な土台にもなります。
ここから先は、 「自分が欲しい機能」を自由に足していくフェーズです。 タグを複数にしたり、期限を付けたり、 メモをカテゴリごとに分けたり—— 同じ DOM+イベント+localStorage のパターンで どこまでも育てていけます。

