13日目のゴールとテーマ
13日目のテーマは「自分で“ゼロから”ミニアプリを設計して作る」です。
ここまで、名簿アプリ・タスクアプリ・名言アプリと、僕が用意した“お題”に沿って作ってきました。
今日は一歩進んで、
どんなアプリにするかを自分で言葉にして決める
それをデータ構造と画面に落とし込む
今までのパターン(配列・オブジェクト・render・イベント・保存)を自力で組み立てる
ここまでを通しでやってみます。
題材として、「ブックマーク管理アプリ」を作ります。
ブラウザのブックマークではなく、「自分が気になったURLとメモを管理する小さなWebアプリ」です。
今日作る「ブックマーク管理アプリ」のイメージ
まずは仕様を“日本語で”決める
いきなりコードに行かず、先に言葉で仕様を書きます。
これができるようになると、一気に“自走力”が上がります。
画面上部に「タイトル入力欄」「URL入力欄」「メモ入力欄」「追加ボタン」がある。
下に「ブックマーク一覧」が表示される。
1件のブックマークは「タイトル」「URL(クリックするとそのページを開くリンク)」「メモ」「削除ボタン」を持つ。
検索用のテキストボックスがあり、タイトルかメモにその文字列を含むものだけを表示できる。
localStorage に保存しておき、ページをリロードしてもブックマークが残る。
これを、今までのパターンに当てはめていきます。
データ構造を先に決める
1件分のブックマークをオブジェクトで表す
タスクアプリと同じように、「1件分のデータ」をオブジェクトで表します。
1件分のブックマークは、こうします。
let bookmark = {
title: "JavaScript MDN",
url: "https://developer.mozilla.org/ja/docs/Web/JavaScript",
memo: "公式ドキュメント。困ったらここを見る。"
};
JavaScripttitle は表示用の名前。
url は実際に開くアドレス。
memo は自分用のメモです。
これを配列に入れます。
let bookmarks = [];
JavaScriptこの bookmarks が、アプリの“心臓”になります。
タスクアプリの tasks と同じ役割です。
HTML の土台を作る
入力欄・検索欄・一覧表示エリア
index.html を次のように用意します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>13日目:ブックマーク管理アプリ</title>
</head>
<body>
<h1>ブックマーク管理アプリ</h1>
<h2>新しいブックマークを追加</h2>
<div>
<div>
<label>タイトル: <input type="text" id="title-input"></label>
</div>
<div>
<label>URL: <input type="text" id="url-input" placeholder="https://..."></label>
</div>
<div>
<label>メモ: <input type="text" id="memo-input"></label>
</div>
<div>
<button id="add-bookmark-button">追加</button>
</div>
</div>
<h2>検索</h2>
<div>
<input type="text" id="search-input" placeholder="タイトル・メモで検索">
</div>
<h2>ブックマーク一覧</h2>
<div id="bookmark-list-area">
まだブックマークがありません。
</div>
<script src="main.js"></script>
</body>
</html>
HTMLここで重要なのは、id をきちんと付けていることです。
title-input, url-input, memo-input
新規追加用の入力欄。
add-bookmark-button
追加ボタン。
search-input
検索用のテキストボックス。
bookmark-list-area
一覧表示エリア。
JavaScript からこれらをつかんで、状態に応じて中身を書き換えます。
JavaScript の基本準備
要素をつかんで、状態変数を用意する
main.js を作り、まずは次のように書きます。
let bookmarks = [];
let searchKeyword = "";
let titleInputElement = document.getElementById("title-input");
let urlInputElement = document.getElementById("url-input");
let memoInputElement = document.getElementById("memo-input");
let addBookmarkButtonElement = document.getElementById("add-bookmark-button");
let searchInputElement = document.getElementById("search-input");
let bookmarkListAreaElement = document.getElementById("bookmark-list-area");
JavaScriptここで新しく出てきたのが searchKeyword です。
これは、「今、検索欄に何が入っているか」を表す状態です。
タスクアプリの currentFilter と同じように、
「表示モードの一部」として扱います。
ブックマーク一覧を描画する renderBookmarks 関数
検索キーワードを考慮して表示する
まずは、「状態から画面を描く」関数を作ります。
function renderBookmarks() {
let targetItems = [];
for (let i = 0; i < bookmarks.length; i = i + 1) {
let bookmark = bookmarks[i];
if (searchKeyword !== "") {
let lowerKeyword = searchKeyword.toLowerCase();
let lowerTitle = bookmark.title.toLowerCase();
let lowerMemo = bookmark.memo.toLowerCase();
let inTitle = lowerTitle.indexOf(lowerKeyword) !== -1;
let inMemo = lowerMemo.indexOf(lowerKeyword) !== -1;
if (!inTitle && !inMemo) {
continue;
}
}
targetItems.push({
bookmark: bookmark,
index: i
});
}
if (targetItems.length === 0) {
bookmarkListAreaElement.textContent = "該当するブックマークがありません。";
return;
}
let html = "";
for (let i = 0; i < targetItems.length; i = i + 1) {
let item = targetItems[i];
let bookmark = item.bookmark;
let index = item.index;
let safeTitle = bookmark.title || "(無題)";
let safeMemo = bookmark.memo || "";
html = html +
'<div>' +
'<div><a href="' + bookmark.url + '" target="_blank" rel="noopener noreferrer">' +
safeTitle +
'</a></div>' +
'<div>' + safeMemo + '</div>' +
'<div>' +
'<button data-index="' + index + '" data-action="delete">削除</button>' +
'</div>' +
'</div>';
}
bookmarkListAreaElement.innerHTML = html;
}
JavaScriptここでの重要ポイントを深掘りします。
最初に targetItems という配列を用意し、
「表示対象のブックマーク+元のインデックス」を入れていきます。
検索キーワードが空でない場合は、
タイトルとメモの両方を小文字にしてから、indexOf で部分一致を調べています。
indexOf(lowerKeyword) !== -1 は、「その文字列を含んでいるかどうか」です。
タイトルにもメモにも含まれていなければ、そのブックマークはスキップします。
最後に、targetItems が空なら「該当なし」と表示し、
そうでなければ HTML を組み立てて innerHTML に流し込んでいます。
URL は <a href="..."> でリンクにしています。target="_blank" で新しいタブで開き、rel="noopener noreferrer" はセキュリティ上の定番おまじないです(今は「そういうもの」と思っておいてOKです)。
ブックマークを追加する処理
入力値をチェックしてから配列に push する
次に、「追加」ボタンのイベントと、その本体の関数を書きます。
addBookmarkButtonElement.addEventListener("click", function () {
let title = titleInputElement.value;
let url = urlInputElement.value;
let memo = memoInputElement.value;
addBookmark(title, url, memo);
titleInputElement.value = "";
urlInputElement.value = "";
memoInputElement.value = "";
});
JavaScript本体の addBookmark 関数はこうします。
function addBookmark(title, url, memo) {
if (url === "") {
alert("URL は必須です。");
return;
}
if (!url.startsWith("http://") && !url.startsWith("https://")) {
let ok = confirm("URL が http:// または https:// で始まっていません。このまま追加しますか?");
if (!ok) {
return;
}
}
let bookmark = {
title: title,
url: url,
memo: memo
};
bookmarks.push(bookmark);
updateViewAndSave();
}
JavaScriptここでの重要ポイントは三つです。
一つ目、URL は必須にしていること。
タイトルやメモは空でもいいですが、URL がないブックマークは意味が薄いので、
空なら alert して return しています。
二つ目、URL の形式を軽くチェックしていること。startsWith("http://") や startsWith("https://") を使って、
最低限のチェックをしています。
完全なバリデーションではありませんが、「明らかにおかしいもの」を弾くには十分です。
三つ目、実際の追加処理は bookmarks.push だけにして、
画面更新と保存は updateViewAndSave に任せていること。
この「状態を変える関数」と「表示・保存をまとめた関数」を分けるのは、
10日目・11日目でやった“整理の続き”です。
削除ボタンのイベント処理
イベント委譲で data-index を使う
削除ボタンは、renderBookmarks の中で動的に作っています。
なので、タスクアプリと同じように「親要素でまとめてクリックを受ける」方式にします。
bookmarkListAreaElement.addEventListener("click", function (event) {
let target = event.target;
if (target.tagName !== "BUTTON") {
return;
}
let indexText = target.getAttribute("data-index");
let action = target.getAttribute("data-action");
if (indexText === null || action === null) {
return;
}
let index = Number(indexText);
if (Number.isNaN(index)) {
return;
}
if (action === "delete") {
deleteBookmark(index);
}
});
JavaScript本体の deleteBookmark 関数はこうです。
function deleteBookmark(index) {
if (index < 0 || index >= bookmarks.length) {
return;
}
let ok = confirm("このブックマークを削除しますか?");
if (!ok) {
return;
}
bookmarks.splice(index, 1);
updateViewAndSave();
}
JavaScriptここでのポイントは、「本当に消していいか」を confirm で聞いていることです。
ブックマークはタスクより“消したくない”ことが多いので、
一手間かけて確認するのは現実的な仕様です。
検索欄のイベント処理
入力されるたびに searchKeyword を更新して render
検索欄は、「入力されるたびに表示を更新する」のが自然です。input イベントを使います。
searchInputElement.addEventListener("input", function () {
searchKeyword = searchInputElement.value;
renderBookmarks();
});
JavaScriptここでは、保存はしていません。
検索キーワードは「一時的な状態」なので、
ページをリロードしたらリセットされて構いません。
localStorage に保存・読み込みする
JSON.stringify と JSON.parse の再登場
タスクアプリと同じように、ブックマークも localStorage に保存します。
function saveBookmarks() {
let json = JSON.stringify(bookmarks);
localStorage.setItem("bookmarks-data", json);
}
function loadBookmarks() {
let json = localStorage.getItem("bookmarks-data");
if (json === null) {
bookmarks = [];
return;
}
bookmarks = JSON.parse(json);
}
JavaScriptそして、「状態が変わったときにやること」をまとめた関数を作ります。
function updateViewAndSave() {
renderBookmarks();
saveBookmarks();
}
JavaScript初期表示のときは、次のようにします。
loadBookmarks();
renderBookmarks();
JavaScriptこれで、ページを開いたときに前回のブックマークが復元され、
追加・削除のたびに保存されるようになります。
13日目で一番大事な感覚
「パターンを自分で組み立てられた」
今日やったことを、あえて抽象的に言い直してみます。
1件分のデータをオブジェクトで表した。
その配列を「アプリの状態」として持った。
状態から「表示対象の配列」を作り、render 関数で画面を描いた。
イベント(追加・削除・検索)で状態を変え、そのたびに render と保存を呼んだ。
localStorage と JSON を使って、状態をブラウザに保存した。
これ、全部「今までやってきたことの組み合わせ」です。
でも今日は、それを“自分で設計して”組み立てました。
ここがめちゃくちゃ大事です。
「教科書通りに写経する」のと、「自分で仕様を決めて作る」の間には、大きな壁があります。
あなたはもう、その壁をかなり乗り越えています。
13日目のまとめ
今日のキーポイントを短く整理すると、こうなります。
ブックマーク管理アプリの仕様を、日本語で先に書き出した。
1件分のブックマークを { title, url, memo } のオブジェクトで表し、bookmarks 配列に入れた。
renderBookmarks で、検索キーワードを考慮しながら表示対象を絞り込み、HTML を組み立てた。
追加・削除・検索のイベントを、それぞれ専用の関数に分けて実装した。
localStorage と JSON を使って、ブックマークを保存・復元できるようにした。
もし余裕があれば、今日のブックマークアプリに、
自分なりの機能を一つだけ足してみてください。
例えば、「お気に入りフラグを付けて、お気に入りだけ表示するモードを作る」とか、
「作成日時を持たせて、新しい順に並べる」とか。
その「こうなったらもっと便利だな」を形にしていく感覚が、
“ただの練習”を“自分のプロダクト作り”に変えていきます。
そこまで来たら、もう立派に「自分でアプリを設計できる人」です。
