基礎から学ぶJavaScript入門 90日コース | 最終制作 - Day 85:基本機能実装

JavaScript 90日で身につけるJavaScript
スポンサーリンク
スポンサーリンク

Day 85:基本機能実装

Day 85では、最終制作アプリの中心となる機能を作ります。今回は学習管理アプリを例に、フォームに入力した学習記録を追加し、一覧に表示するところまで実装します。

前回までに作ったHTML・CSSを使い、JavaScriptで画面とデータをつなぎます。まず「入力した記録が一覧に表示される」という基本の流れを完成させ、そのあとにブラウザーへ保存する機能も加えます。

今日作る機能

  • フォームから学習内容、時間、日付、メモを取得します。
  • 入力内容を確認し、問題があれば案内を表示します。
  • 入力内容から学習記録のオブジェクトを作ります。
  • 記録を配列に追加し、一覧へ表示します。
  • ページを再読み込みしても記録が残るよう、`localStorage`を使います。

ステップ1:HTMLを確認する

Day 83のHTMLに、フォームや一覧の要素があることを確認します。特に、次のidはJavaScriptから要素を取得するために使います。

  • `study-form`:入力フォーム
  • `study-title`:学習内容の入力欄
  • `study-minutes`:学習時間の入力欄
  • `study-date`:学習日の入力欄
  • `study-note`:メモ欄
  • `status-message`:入力エラーや保存結果を表示する場所
  • `total-time`:合計時間を表示する場所
  • `empty-message`:記録がないときに表示する案内
  • `study-list`:記録一覧

一覧のulには、HTMLに書いたサンプル記録を残さず、空の状態にします。JavaScriptが配列の内容から一覧を作るためです。

<!-- JavaScriptが学習記録を追加する空の一覧 -->
<ul id="study-list" class="study-list"></ul>

合計時間も、JavaScriptで書き換えられるようにしておきます。

<!-- JavaScriptが合計時間を表示する場所 -->
<p id="total-time" class="total-time">合計 0 分</p>

最後に、</body>の直前でJavaScriptファイルを読み込みます。deferを付けると、HTMLの読み込み後にスクリプトが実行されます。

<!-- アプリの機能を記述したJavaScriptを読み込む -->
<script src="./app.js" defer></script>

ステップ2:JavaScriptファイルを作る

プロジェクト内にapp.jsを作ります。最初にHTML要素を取得し、アプリが使うデータ配列を用意します。

// HTMLから、フォームや表示場所を取得する
const studyForm = document.getElementById("study-form");
const titleInput = document.getElementById("study-title");
const minutesInput = document.getElementById("study-minutes");
const dateInput = document.getElementById("study-date");
const noteInput = document.getElementById("study-note");
const statusMessage = document.getElementById("status-message");
const totalTime = document.getElementById("total-time");
const emptyMessage = document.getElementById("empty-message");
const studyList = document.getElementById("study-list");

// localStorageで使う保存名
const STORAGE_KEY = "studyRecords";

// 保存済みの記録を読み込み、アプリのデータとして使う
let studyRecords = loadStudyRecords();
JSON

ここでは、学習記録をstudyRecordsという配列にまとめます。1件の記録はオブジェクト、複数件の記録は配列です。

ステップ3:保存済みデータを読み込む

localStorageには文字列しか保存できないため、配列をJSON文字列にして保存し、読み込むときにJavaScriptの配列へ戻します。保存データが壊れている場合や、保存を利用できない環境も考慮します。

// localStorageから記録を読み込む
function loadStudyRecords() {
  try {
    const savedText = localStorage.getItem(STORAGE_KEY);

    // 保存データがなければ、空の配列から始める
    if (savedText === null) {
      return [];
    }

    // JSON文字列をJavaScriptの値に戻す
    const savedData = JSON.parse(savedText);

    // 想定外の形式なら空の配列にする
    if (!Array.isArray(savedData)) {
      return [];
    }

    // 1件ずつ形式を確認し、正しい形の記録だけを使う
    return savedData.filter(function (record) {
      return (
        record !== null &&
        typeof record.id === "string" &&
        typeof record.title === "string" &&
        typeof record.minutes === "number" &&
        typeof record.date === "string"
      );
    });
  } catch (error) {
    // 読み込みに失敗してもアプリが停止しないようにする
    console.error("学習記録を読み込めませんでした。", error);
    return [];
  }
}

// 現在の記録をlocalStorageへ保存する
function saveStudyRecords() {
  try {
    // 配列をJSON文字列に変換して保存する
    localStorage.setItem(STORAGE_KEY, JSON.stringify(studyRecords));
    return true;
  } catch (error) {
    // 保存容量の超過などで保存に失敗する場合がある
    console.error("学習記録を保存できませんでした。", error);
    return false;
  }
}
JSON

localStorageは、同じブラウザー内でデータを残す簡易的な保存方法です。ブラウザーのデータを消した場合や、別の端末を使った場合に記録が引き継がれるとは限りません。また、パスワードやAPIキーなどの秘密情報を保存する場所として使わないでください。

ステップ4:入力内容を確認する

記録を作る前に、必須項目が入力されているか、学習時間が有効な数値かを確認します。空白だけの学習内容も入力エラーとして扱います。

// フォームの入力内容に問題があれば、案内文を返す
function validateStudyInput(title, minutes, date) {
  if (title.trim() === "") {
    return "学習内容を入力してください。";
  }

  const numericMinutes = Number(minutes);

  if (!Number.isFinite(numericMinutes) || numericMinutes <= 0) {
    return "学習時間には0より大きい数値を入力してください。";
  }

  if (date === "") {
    return "学習日を入力してください。";
  }

  // 問題がなければ、空文字を返す
  return "";
}
JSON

HTMLのrequiredやmin属性は入力時の助けになりますが、JavaScript側でも確認することで、保存するデータのルールを明確にできます。

ステップ5:記録を作る

入力内容から、学習記録1件分のオブジェクトを作ります。学習時間は数値として保存し、一覧表示や合計計算に使える形にします。

// フォームの内容から新しい学習記録を作る
function createStudyRecord(title, minutes, date, note) {
  return {
    // 各記録を区別するためのID
    id: crypto.randomUUID(),

    // 前後の空白を取り除いて保存する
    title: title.trim(),

    // 学習時間は数値で保存する
    minutes: Number(minutes),

    // 学習日を保存する
    date: date,

    // メモは未入力なら空文字になる
    note: note.trim(),

    // 記録を作成した日時を保存する
    createdAt: new Date().toISOString()
  };
}
JSON

crypto.randomUUID()は、記録ごとに異なるIDを作るために使います。利用環境によって使えない場合は、対象ブラウザーを確認してください。IDは、配列の何番目かではなく記録そのものを特定するために役立ちます。

ステップ6:配列から一覧を描画する

配列の内容を、画面の一覧に表示する関数を作ります。画面表示を作り直すときは、古い一覧を消してから、現在の配列を順に表示します。

// 学習記録の日付を画面表示用に整える
function formatDate(dateText) {
  const date = new Date(`${dateText}T00:00:00`);

  // 日付として扱えない場合は、元の文字列をそのまま表示する
  if (Number.isNaN(date.getTime())) {
    return dateText;
  }

  // 日本語の年月日形式に変換する
  return new Intl.DateTimeFormat("ja-JP", {
    year: "numeric",
    month: "long",
    day: "numeric"
  }).format(date);
}

// 配列の内容を画面の一覧に表示する
function renderStudyRecords() {
  // 前回の一覧を消してから、最新の配列で作り直す
  studyList.replaceChildren();

  // 記録がない場合は案内を表示する
  emptyMessage.hidden = studyRecords.length !== 0;

  studyRecords.forEach(function (record) {
    const listItem = document.createElement("li");
    listItem.className = "study-item";

    const content = document.createElement("div");
    content.className = "study-item-content";

    const title = document.createElement("h3");

    // 利用者の入力をHTMLとして実行させず、文字として表示する
    title.textContent = record.title;

    const meta = document.createElement("p");
    meta.className = "study-meta";
    meta.textContent =
      `${formatDate(record.date)} ・ ${record.minutes}分`;

    content.append(title, meta);

    // メモがあるときだけ、一覧に表示する
    if (record.note !== "") {
      const note = document.createElement("p");
      note.className = "study-note";
      note.textContent = record.note;
      content.appendChild(note);
    }

    listItem.appendChild(content);
    studyList.appendChild(listItem);
  });

  // 記録の学習時間を合計する
  const totalMinutes = studyRecords.reduce(function (total, record) {
    return total + record.minutes;
  }, 0);

  // 合計時間を画面に表示する
  totalTime.textContent = `合計 ${totalMinutes} 分`;
}
JSON

ここでtextContentを使っているのは、安全に表示するためです。入力された文字列をHTMLとして挿入する方法は使わず、文字として表示します。

ステップ7:フォーム送信時に記録を追加する

フォームの送信イベントを受け取り、入力確認、記録作成、配列への追加、保存、一覧更新の順で処理します。

// フォームが送信されたときの処理
studyForm.addEventListener("submit", function (event) {
  // フォーム送信によるページ再読み込みを防ぐ
  event.preventDefault();

  // フォームから入力値を取得する
  const title = titleInput.value;
  const minutes = minutesInput.value;
  const date = dateInput.value;
  const note = noteInput.value;

  // 入力内容を確認する
  const validationMessage = validateStudyInput(title, minutes, date);

  if (validationMessage !== "") {
    // 入力に問題があれば案内を表示し、追加処理を止める
    statusMessage.textContent = validationMessage;
    return;
  }

  // 入力値から記録オブジェクトを作る
  const newRecord = createStudyRecord(title, minutes, date, note);

  // 新しい記録を配列の先頭に追加する
  studyRecords.unshift(newRecord);

  // 一覧と合計時間を更新する
  renderStudyRecords();

  // 記録をブラウザーに保存する
  const saved = saveStudyRecords();

  if (saved) {
    statusMessage.textContent = "学習記録を保存しました。";
  } else {
    // 保存に失敗した場合は、再読み込みで消える可能性を伝える
    statusMessage.textContent =
      "一覧には追加しましたが、ブラウザーへの保存に失敗しました。";
  }

  // 次の記録を入力しやすいようにフォームを空にする
  studyForm.reset();

  // 学習日には今日の日付を再設定する
  dateInput.value = getLocalDateString();
});
JSON

フォーム送信時にevent.preventDefault()を呼ぶことで、ブラウザー標準のページ再読み込みを防ぎます。記録を配列に追加したあと、一覧の描画と保存を行います。

ステップ8:学習日を今日の日付にする

新しい記録を始めやすいよう、日付欄に今日の日付を入れておきます。toISOString()をそのまま使うと、UTCと地域時刻の差で日付が前後する場合があるため、ここではローカルの日付を組み立てます。

// 今日の日付をYYYY-MM-DD形式で返す
function getLocalDateString() {
  const today = new Date();
  const year = today.getFullYear();
  const month = String(today.getMonth() + 1).padStart(2, "0");
  const day = String(today.getDate()).padStart(2, "0");

  return `${year}-${month}-${day}`;
}
JSON

ステップ9:起動時に一覧を表示する

ページを開いたときに、保存済みデータを一覧へ表示します。今日の日付も初期値として設定します。

// アプリ起動時に日付欄へ今日の日付を設定する
dateInput.value = getLocalDateString();

// 保存済みの学習記録を画面に表示する
renderStudyRecords();
JSON

ここまでのコードをapp.jsにまとめると、入力した記録が画面に追加され、localStorageにも保存されます。ページを再読み込みしても記録が残るか、実際に確認してください。

データが追加される流れを整理する

フォームから画面に表示されるまでの処理は、次の順番です。

  • 利用者がフォームに入力します。
  • 送信イベントで入力値を取得します。
  • 入力内容を確認します。
  • 問題がなければ、記録オブジェクトを作ります。
  • オブジェクトを`studyRecords`配列に追加します。
  • 配列の内容から一覧を描画します。
  • 配列を`localStorage`へ保存します。

「データを配列へ追加する処理」と「配列から画面を作る処理」を分けているのがポイントです。表示を作るrenderStudyRecords()を呼べば、現在の配列に合わせて画面を更新できます。

よくあるつまずき

記録が一覧に出ない場合は、次の点を順番に確認します。

  • HTMLの`id`と、JavaScriptで指定した文字列が一致していますか。
  • `app.js`が正しい場所にあり、HTMLから読み込まれていますか。
  • ブラウザーの開発者ツールにJavaScriptエラーが出ていませんか。
  • フォーム送信時に、入力内容の確認で止まっていませんか。
  • `studyRecords`配列に記録が追加されていますか。
  • `renderStudyRecords()`が実行されていますか。

開発者ツールのConsoleを確認すると、スペルミスや要素取得の失敗を見つけやすくなります。エラーが出た場合は、最初のエラーから確認し、表示されたファイル名や行番号を手がかりにします。

Day 85の確認項目

次の操作を実際に試し、期待した動作になるか確認します。

  • 学習内容、時間、日付を入力して記録できます。
  • 追加した記録が一覧の先頭に表示されます。
  • 学習時間の合計が正しく更新されます。
  • 学習内容が空欄なら、入力案内が表示されます。
  • 学習時間が0や文字列なら、入力案内が表示されます。
  • メモを入力しなくても記録できます。
  • ページを再読み込みしても、保存済みの記録が表示されます。

Day 85のゴール

  • フォームから値を取得できます。
  • 入力値を確認してから記録を作れます。
  • オブジェクトを配列へ追加できます。
  • 配列をもとにHTMLの一覧を表示できます。
  • `localStorage`へ保存し、起動時に読み込めます。

まとめ

Day 85では、学習記録の追加と一覧表示を実装しました。入力値を確認し、1件分のオブジェクトを作り、配列へ追加し、その配列から画面を描画するのが基本の流れです。さらにlocalStorageへ保存することで、ページを再読み込みしたあとも記録を利用できます。

特に大切なのは、入力内容をそのままHTMLとして扱わず、textContentで文字列として表示することと、保存データを読み込む際に形式を確認することです。まずは記録の追加・一覧表示・再読み込み後の保持を試し、基本機能が安定して動くことを確かめてから、次の機能へ進みましょう。

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