URLで検索結果を表す

JavaScriptBeginner
オンラインで実践に進む

はじめに

Fieldnote は週末の小さなイベント案内サイトです。便利な検索リンクは、キーワード、カテゴリ、並び順、結果ページという表示内容を表す必要があります。友人がそのリンクを開いたら、同名の2つのイベントについても正しい時刻と場所を含め、同じイベントが表示されるべきです。用意されたレスポンシブなサイトと小規模なローカルカタログを使い、この状態の対応関係を作ります。

ターミナルの基本的なパス、HTMLフォーム、CSSのレイアウトとフォーカス、JavaScriptの配列、オブジェクト、関数、DOM選択、イベントの基礎が前提です。このラボでは配列の絞り込み、比較関数、ページ境界、URL、URLSearchParams を学びます。検索やURL解析には多くの用途がありますが、ここで作るのは4つの値で再現できる完全なビューです。用意されたレンダラーは、あなたのコードが選んだレコードからカードを作ります。バックエンドやフレームワークの知識は不要です。イベントはすべて合成データです。55分という時間は、読解や通常の間違いへの対応を含む作者の見積もりです。

コマンドには Workspace、入口ページには Fieldnote を使います。このラボは独立して初期化されます。編集するのは /home/labex/project/fieldnote/work/selection.js、state.js、url.js だけです。app.js はこのラボでは準備済みのコントローラーであり、次のラボであなたの作業対象になります。catalog.js、view.js、index.html、style.css、独立したStudyビュー、準備済みサービスを保持してください。コントローラーはすでにフォームを送信してページを描き直し、replaceState で現在の履歴項目を更新します。ここでのブラウザー履歴は準備環境の動作で、まだ学習要件ではありません。サイトのラベルは英語のままです。

実際のレコードを安定した順序で選ぶ

このステップでは、動作する共有ビューを開いてから、作業ページのキーワード、カテゴリ、並び順を予測できるようにします。

Fieldnote で Open Study view in a new page を選びます。この実際のリンクは、製品の埋め込みタブの外に独立したブラウザーページを開きます。サイト上の操作はその新しいページで行います。アドレスには q、category、sort、page が含まれ、キーワードは print & fold、カテゴリは Make、表示は Page 2 of 2 で、Print & Fold: map edition と Print & Fold が並びます。後者は North room の14:00のイベントで、同名の以前のイベントではありません。結果の下の Open this view in a new page を選びます。新しいページでもコントロール、ページ、タイトル、日付、場所が同じであることを確認してください。リンクはこのラボとプロキシ経路が稼働している間だけ使えます。これは独立した例であり、あなたの作品や採点結果ではありません。

以下の独立したStudyビューには、2ページ目の2レコードが表示されています。タイトルだけでなく、日付、時刻、場所も比べてください。

2ページ目のレコードを表示する独立したStudyビュー

追加で開いた例のページを閉じます。Fieldnote の入口で Open Work view in a new page を選び、この独立した作業ページを開いたままにします。ファイル編集では製品の Workspace タブに戻ります。タブの切り替えは、サイトのリンクを開くことやブラウザーのBackボタンを押すこととは異なります。Studyが動いていても、あなたの作業は未完成から始まります。

cd はこのターミナルのディレクトリを変えます。絶対パスは / で始まります。pwd は現在のディレクトリを表示し、cat は小さなテキストファイルを変更せずに表示します。

cd /home/labex/project/fieldnote/work
pwd
cat catalog.js

3つのレコードを読みます。FN-101 と FN-102 はどちらも 2026-11-14 の 10:00 に始まります。FN-108 は FN-102 と同じタイトルですが、日付が後です。カタログの入力順は、意図的に表示順と異なっています。print & fold、カテゴリ make、日時順の場合の最初の3件を予測してください。FN-101、FN-102、FN-104 です。カード数だけでなく、実際のレコードの識別を比べます。

カタログを変更せずに絞り込みます。filter は一致するオブジェクトの新しい配列を作ります。includes は文字どおりの部分文字列を調べ、toLowerCase により大文字と小文字を区別せず検索します。検索対象は1つに連結した文字列 title + ' ' + description です。そのため、間をつなぐ空白をまたぐ一致も認めます。&& は両方の条件、|| はいずれかの条件が成立することを意味します。all はどのカテゴリも許可します。アロー関数は各イベントを受け取ります。

sort は並べ替える配列自体を変更します。新しく絞り込んだ配列を並べ替えれば、準備されたカタログは保持されます。比較関数は負の数、ゼロ、正の数を返します。以下の補助関数ではJavaScript文字列の < と > を使います。日時順は date + 'T' + time を比較し、同じ場合はASCIIのイベントIDで決めます。タイトル順は小文字化したタイトルを比較し、次にIDを比較します。この固定の文字列順は、ブラウザーの言語設定や元の配列位置に依存しません。準備された日付は同一タイムゾーンです。タイムゾーン間の予定調整は実装しません。

条件式 condition ? first : second は値を1つ選びます。ここでの primary || ... は、主比較がゼロの場合だけID比較を使います。CATALOG はあなたのファイルより前に読み込まれる準備済みオブジェクトです。state は q、category、sort、page を持つオブジェクトで、次のステップで値を安全で一貫したものにします。

完全な関数を selection.js に保存します。cat > filename はそのファイルを置き換えます。引用符付きの <<'JS' はヒアドキュメントの開始です。最後の JS だけを1行に書くまで貼り付けます。区切りを引用するとJavaScriptの記号がそのまま保持されます。これはターミナルで編集・保存する操作で、WebIDE内部のエディターを自動操作するものではありません。

cat > selection.js <<'JS'
function compare(a, b) {
  return a < b ? -1 : a > b ? 1 : 0;
}
function matches(state) {
  return CATALOG.events.filter(event =>
    (state.category === 'all' || event.category === state.category) &&
    (event.title + ' ' + event.description).toLowerCase()
      .includes(state.q.toLowerCase())
  ).sort((a, b) => {
    const primary = state.sort === 'title'
      ? compare(a.title.toLowerCase(), b.title.toLowerCase())
      : compare(a.date + 'T' + a.time, b.date + 'T' + b.time);
    return primary || compare(a.id, b.id);
  });
}
JS

独立した作業ページに戻り、再読み込みします。コントロールとカードを待ちます。Keyword に print & fold を入力し、Category で Make を選び、Soonest first のまま Search を実行します。予測した3件が表示され、10:00の2件はタイトルと場所で区別できます。Sort by を Title A–Z に変えて再送信すると、Print & Fold、Print & Fold、Print & Fold: colour の順になります。日付を読むと、同名の2件は FN-102、次に FN-108 です。選択欄を変えただけでは下書きです。Search で3つのフィルターをまとめて確定します。共有URLはまだ完成していません。

結果が空なら、まずキーワードとカテゴリを確認します。prnit のようなタイプミスに一致がないのは正しい動作です。print & fold に直して再送信してください。編集後に反応しなくなったら Workspace へ戻り、cat selection.js を実行し、完全なコードと括弧・引用符を比べて再度置き換えます。保存後に作業ページを再読み込みします。VMのリセットは不要です。

両方の並び順を観察してから、このステップの Check を使います。元のフロントエンドファイルを使い捨てのブラウザーテスト環境で実行し、完全なレコードの関係を検証します。現場のページを補完したり、Studyをあなたの成果として数えたりはしません。

フィルターとページ境界を正規化する

このステップでは下書きや受け取った値を予測可能な状態へ変換し、空の結果や最後の利用可能ページも扱います。

独立した作業ページで、print & fold、Make、Soonest first を送信し、Next を選びます。一致は5件、1ページは3件なので、2ページ目は最後の2件です。1ページ3件という値は準備データ CATALOG.pageSize です。レンダラーは配列の slice(start, end) で全体を変更せずに1ページを取り出します。添字はゼロ始まりで終端は含みません。1ページ目は slice(0, 3)、2ページ目は slice(3, 6) です。各カードは選ばれたイベントオブジェクトと、そのID、タイトル、説明、日付、時刻、場所を使います。

受け取るページが 999、カテゴリが未知、キーワードの両端に空白がある場合を考えます。正規化関数は描画前にそれらの公開ルール上の意味を決めます。キーワードがなければ空にし、両端の空白だけを除去して、内部の空白とユーザーの大文字・小文字表記を保ちます。一致判定はこれまでどおり大文字・小文字を区別しません。未知または欠落したカテゴリは all、未知または欠落した並び順は soon になります。ページは Number で変換し、2.0 のような同等表記も含め、正の安全な整数を許可します。無効、ゼロ、負、整数でない値、安全でない値は1になります。有効だが大きすぎるページは最後の一致ページまでに制限します。ゼロ件でも1ページ中の1ページ目とし、両方のページボタンを無効にします。

Number.isSafeInteger は正確に表現できない整数値を避けます。Math.ceil は切り上げです。5件を3で割ると2ページになります。Math.max(1, ...) は空の結果でもページ数を1に保ち、Math.min は要求ページを制限します。Array.includes は許可されたカテゴリや並び順のリストへの所属も調べます。|| の既定値は、欠落・空の入力を扱います。正規化状態には値を入れ、すでにURLエンコードされた文字列は入れません。

Workspace に切り替えます。ディレクトリは引き続き /home/labex/project/fieldnote/work です。state.js を次の完全な関数に置き換えます。

cat > state.js <<'JS'
function normalize(raw) {
  const state = {
    q: (raw.q || '').trim(),
    category: CATALOG.categories.includes(raw.category) ? raw.category : 'all',
    sort: ['soon', 'title'].includes(raw.sort) ? raw.sort : 'soon',
    page: Number(raw.page || 1)
  };
  if (!Number.isSafeInteger(state.page) || state.page < 1) state.page = 1;
  const pages = Math.max(1, Math.ceil(matches(state).length / CATALOG.pageSize));
  state.page = Math.min(state.page, pages);
  return state;
}
JS

これはあなたの matches を呼び、絞り込み後に境界を計算します。元のカタログを並べ替えません。q: ' print & fold '、category: 'make'、sort: 'soon'、page: '999' の状態を予測してください。キーワードの両端だけが取り除かれ、カテゴリと並び順は同じ、ページは2です。page: '2.5' なら1です。URLを読む機能ができる次のステップで、受け取ったURL値を実際に試します。

作業ページへ戻り再読み込みします。Keyword に空白2つ、print & fold、空白2つを入力します。Make を選び、Search を実行します。送信後は両端の空白なしの print & fold が表示されるはずです。Next を選ぶと地図版と North room の14:00のイベントだけが残り、Next は無効になります。2ページ目から sound + light、Listen、Title A–Z を検索すると、新しいフィルターが確定し Page 1 of 1 に戻ります。文字としてのプラスはタイトルの一部であり、空白ではありません。no such constellation を検索します。カードはなく、読みやすいゼロ件メッセージ、1ページ中1ページ目、両ボタン無効となります。これは正常な結果です。一致するキーワードとカテゴリを戻して送信すれば復帰します。

Check 前に、元のカタログと全レコード内容が保持されていることを確認します。未知値と数字のURL表記の検証は次のステップで加わります。このステップは正規化したフォームと到達可能なページ境界を検証します。

実際のビューURLを読み書きする

このステップでは、直接開いたリンクから4つの確定値とその可視レコードを復元します。

URLにはオリジン、パス、クエリ、そして # の後の任意のフラグメントがあります。LabExプロキシが実際のオリジンとパスを提供します。オリジンに / を足して新しいベースを推測しないでください。new URL(location.href) はこのページの本当のアドレスから始めます。searchParams は get でデコード済みのクエリ値を読み、set で通常の未エンコード値を書きます。キーワード中の & などの区切りをエンコードし、文字としてのプラスを %2B にします。空白は + または %20 で直列化できます。set より前の encodeURIComponent は二重エンコードになるので、元の値を渡します。

このコースが管理するのは q、category、sort、page だけです。追加のクエリパラメーターと既存フラグメントをすべて保ちます。それらはフィルター状態の外にあり、プラットフォームのものかもしれません。欠落値や既定値は省略しても明記しても構いません。パラメーター順、等価な数字ページ、2種類の空白表記は意味を変えません。初期化は余分な履歴項目を作らずにURLを正規化できます。このラボでは準備済みコントローラーが置換を行い、履歴APIの呼び出しは次のラボで学びます。

以下の for...of は、既知の4つのキー文字列を順に処理します。delete は、書き込み側が既定値以外を設定する前に古い管理対象クエリ値を除去します。

Workspace で、読み書きの関数を url.js に保存します。

cat > url.js <<'JS'
function readURL() {
  const params = new URL(location.href).searchParams;
  return normalize({q: params.get('q'), category: params.get('category'),
    sort: params.get('sort'), page: params.get('page')});
}
function stateURL(state) {
  const url = new URL(location.href);
  for (const key of ['q', 'category', 'sort', 'page']) url.searchParams.delete(key);
  if (state.q) url.searchParams.set('q', state.q);
  if (state.category !== 'all') url.searchParams.set('category', state.category);
  if (state.sort !== 'soon') url.searchParams.set('sort', state.sort);
  if (state.page !== 1) url.searchParams.set('page', state.page);
  return url;
}
JS

先にキーを削除すると、既定値を選んだときに古いフィルターが残らず、未知のキーは保持できます。条件行が省略するのは管理対象の既定値だけです。書き込み関数は URL オブジェクトを返し、自分では移動しません。準備済みコントローラーがアドレスを変え、その後レンダラーが実際の共有欄とリンクを更新します。

作業ページに戻り再読み込みします。print & fold、Make、Title A–Z を検索し、Next を選びます。共有欄には sort=title と page=2 を含む4つの現在値すべてが表されます。Open this view in a new page を選び、新ページを再読み込みします。タイトル順の最後の2件、Print & Fold: map edition、Print & Fold: tiny type の順になるはずです。件数だけでなく日付と場所を確認します。これは作業ファイルへの実際のリンクです。独立したCheckも、キャッシュ済みのローカル設定に頼らず、クリーンなブラウザーコンテキストで開きます。

受け取る値のテストには独立ページのアドレスバーを使います。実際のオリジンと /work/ を維持し、? の後のクエリだけを置き換えます。クエリの後に #directory-title を付け、フラグメント保持を確認します。まず次を使います。

?edition=november&platform=keep-me&q=print%20%26%20fold&category=make&sort=soon&page=999#directory-title

2ページ目の地図版と North room のイベントが表示され、Soonest first が選択されます。正規化アドレスでは sort=soon を省略したり、空白を + にしたりして構いません。edition、platform、フラグメントは残ります。次にクエリを以下に置き換えます。

?edition=november&category=unknown&sort=unknown&page=2.5#directory-title

空のキーワード、All categories、Soonest first、Page 1 of 4、最初のレコード FN-101、FN-102、FN-105 になるはずです。未知値は既定値になるので、リセットが必要なエラーではありません。最後に、最初のテストのキーワードとカテゴリで page=2.0 を使うクエリは2ページ目と同じ意味です。順序と既定値表記は柔軟ですが、正確な可視状態と完全なレコードは変えられません。

独立した作業ページを読み取り用に残します。約768ピクセル、その後390ピクセルの幅で、フィルター、ページ操作、共有リンク、完全なレコード文字列を横スクロールせずに利用できることを確認します。Tab を押してフォーカスの見やすさを観察します。外枠とスタイルは準備済みなので保ちます。Check を実行してください。このラボは直接URLの復元、現在のURL・コントロール・結果の一致、安全な境界を検証します。まだBack/Forwardの復元は要求しません。準備済みコントローラーは現在項目を置換し、これが次のラボの出発点になります。

まとめ

同値のときも安定して順序を決め、完全なイベントレコードを選び、フィルターとページ境界を正規化し、実際のビューURLで4値を読み書きしました。同等なURL表記は元のカタログ順に依存せず同じビューを再現します。次のラボではコントローラーを置き換え、実際のブラウザーBackとForwardでも復元できるようにします。