7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Wano GroupAdvent Calendar 2024

Day 13

URLをヒストリーとして保持しつつ、1ページで複数の検索を別々に走らせるために頑張った話

Last updated at Posted at 2024-12-12

これは、Wano Group Advent Calendar 2024 の13日目の記事です。
12日目は、@rom1000_onigiriさんの、企業の成長フェーズと並走するUIライブラリとの付き合い方です。

概要

弊社では、カラオケやミュージックビデオの配信ができるサービスVideo Kicksを開発・運営しています。

今回は、1画面で複数の検索を別々に走らせることと、検索条件をヒストリーとして保持してブラウザバック時に検索条件を維持・復元するために頑張った話をします。

環境

"vue": "^3.2.31"
"vue-router": "^4.0.3"

どんな画面?

Video Kicksでは、MVやカラオケ配信のために「リリース」というものを作成します。
マイページ上で今までに作成したリリースを検索することが出来ます。
詳細は後述しますが、この画面には検索ブロックが3つ存在し、それぞれが必要に応じて別々に検索APIを叩きます。

必要要件

今回の開発では、以下の要件を実現する必要がありました。

  • ブラウザバック・フォワード時に、以前入力した検索条件を維持する
  • リリース一覧は、条件によって3つのブロック「更新可能リリース」「作成中」「リリース一覧」に分かれている
  • ブロックごとに個別の検索条件を持つ
  • キーワード検索のみ、3つ全てのブロックで絞り込みを実行する

実装方針

検索APIとの通信を最小限に抑えるため、検索条件を変更されたブロックのみ検索を走らせたいところです。

そこで以下のような方針で実装を進めました。

  • クエリパラメータを付与し、それをvue-routerのヒストリーとして保存する
    /page/karaoke/release/list?key=テスト&status=20&renewPage=1&editingPage=17&releasePage=2&is_store_addition=false
  • クエリパラメータをvue側で監視し、条件に変更があったブロックのみ検索APIを叩く

各パラメータがどのブロックに影響するかは次の通りです。

全体

  • key・・・キーワード検索

更新可能リリース

  • renewPage・・・ページ番号

作成中

  • editingPage・・・ページ番号

リリース一覧

  • releasePage・・・ページ番号
  • status・・・リリースのステータスID
  • is_store_addition・・・リリースの配信先が追加できるかどうか

以下に実装例を示します。ここでは説明に不要な部分は省略しています。

親コンポーネント

List.vue
<template>
  <div class="label">キーワード検索</div>
  <div class="search-field">
    <div class="search-field__input">
      <vk-input
        @input="(v: string)=> (inputKeyword = v)"
        :value="inputKeyword"
        :placeholder="'キーワード検索'"
        style="width: 100%"
      ></vk-input>
    </div>
    <div class="search-field__btn">
      <vk-btn class="btn" @click="onClickSearchButton">検索</vk-btn>
    </div>
  </div>

  <spacer class="p-20" />

  <list-card
    :title="'更新可能リリース'"
    :keyword="searchKeyword"
    :page="renewPage"
    :limit="limit"
    :isShowSearchParams="false"
    :defaultKaraokeStatus="UIKaraokeStatus.CanRenewal"
    :storeAdditionOnly="false"
    @changePage="newPage => onChangePagination(newPage, 'renew')"
    @onError="store.error"
  />

  <spacer class="p-20" />

  <list-card
    :title="'作成中'"
    :keyword="searchKeyword"
    :page="editingPage"
    :limit="limit"
    :isShowSearchParams="false"
    :defaultKaraokeStatus="UIKaraokeStatus.Editing"
    :storeAdditionOnly="false"
    @changePage="newPage => onChangePagination(newPage, 'editing')"
    @onError="store.error"
  />

  <spacer class="p-20" />

  <list-card
    :title="'リリース一覧'"
    :keyword="searchKeyword"
    :page="releasePage"
    :limit="limit"
    :isShowSearchParams="true"
    :selectedKaraokeStatus="selectedKaraokeStatus"
    :defaultKaraokeStatus="UIKaraokeStatus.All"
    :storeAdditionOnly="storeAdditionOnly"
    @changePage="newPage => onChangePagination(newPage, 'release')"
    @changeAdditionOnly="newBool => onChangeAdditionOnly(newBool)"
    @changeStatus="onChangeKaraokeStatus"
    @onError="store.error"
  />
</template>

<script setup lang="ts">

// 明示的に「検索」を押されたときは、各セクション用のページを1に戻す
function onClickSearchButton() {
  searchKeyword.value = inputKeyword.value;
  renewPage.value = 1;
  editingPage.value = 1;
  releasePage.value = 1;
  addQueryParamsToUrl();
}

// ページの変更時に呼ばれる
function onChangePagination(
  page: number,
  type: "renew" | "editing" | "release"
) {
  if (type == "renew") renewPage.value = page;
  if (type == "editing") editingPage.value = page;
  if (type == "release") releasePage.value = page;
  addQueryParamsToUrl();
}

// リリース一覧 セクションの「ストア追加可能」の変更時に呼ばれる
function onChangeAdditionOnly(bool: boolean) {
  storeAdditionOnly.value = bool;
  addQueryParamsToUrl();
}

// リリース一覧 セクションの「配信ステータス」の変更時に呼ばれる
function onChangeKaraokeStatus(status: UIKaraokeStatus) {
  selectedKaraokeStatus.value = status;
  addQueryParamsToUrl();
}


// URLに検索キーワードを追加。検索状況をブラウザ履歴として保持するために使用
function addQueryParamsToUrl() {
  const url = `${KARAOKE_LIST_PATH}?key=${inputKeyword.value}&status=${selectedKaraokeStatus.value}&renewPage=${renewPage.value}&editingPage=${editingPage.value}&releasePage=${releasePage.value}&is_store_addition=${storeAdditionOnly.value}`;
  router.push(url);
}

// URLのクエリパラメータを取得して、検索フォームの各変数に反映
const setQueryParams = () => {
  const query = route.query;
  inputKeyword.value = (route.query.key as string) || ""; // 初期はURLパラメータからの復元文字がつかわれる
  searchKeyword.value = (route.query.key as string) || "";
  //selectedKaraokeStatusName.value = (route.query.status as string) || "全て";
  selectedKaraokeStatus.value =
    (Number(route.query.status) as UIKaraokeStatus) || UIKaraokeStatus.All;


  renewPage.value = Number(query.renewPage) || 1;
  editingPage.value = Number(query.editingPage) || 1;
  releasePage.value = Number(query.releasePage) || 1;
  storeAdditionOnly.value = Boolean(query.is_store_addition == "true");
};

// URLのクエリパラメータを監視して、変更があればsetQueryParamsを実行
watch(
  () => router.currentRoute.value.query,
  () => setQueryParams()
);


</script>

子コンポーネント

ListCard.vue

<script setup lang="ts">

const props = defineProps<{
  title: string;
  page: number;
  limit: number;
  keyword: string;
  defaultKaraokeStatus: UIKaraokeStatus; // 初期のステータス絞り込み
  isShowSearchParams: boolean; // 配信ステータス・ストア追加可能のフォームを表示するか
  selectedKaraokeStatus?: UIKaraokeStatus;
  storeAdditionOnly?: boolean; // ストア追加可能のみ表示するか
}>();

// キーワード検索と、ブラウザバック時の再検索用
watch(
  [
    () => props.keyword,
    () => props.page,
    () => props.selectedKaraokeStatus,
    () => props.storeAdditionOnly
  ],
  (
    [newKey, newPage, newStatus, newStoreAdditionOnly],
    [oldKey, oldPage, oldStatus, oldStoreAdditionOnly]
  ) => {
    if (newKey != oldKey) {
      page.value = props.page;
      fetchKaraokes(page.value);
      return;
    }

    if (newStatus != oldStatus) {
      if (props.selectedKaraokeStatus) {
        page.value = props.page;
        fetchKaraokes(page.value);
      }
      return;
    }

    if (newPage != oldPage) {
      page.value = newPage;
      fetchKaraokes(newPage);
      return;
    }

    if (newStoreAdditionOnly != oldStoreAdditionOnly) {
      page.value = props.page;
      isAddOnly.value = props.storeAdditionOnly || false;
      fetchKaraokes(page.value);
      return;
    }
  }
);

</script>


処理の流れは次の通りです。
少し複雑になってしまいましたね、よりよい実装方法があればコメントいただけるとありがたいです...

キーワード検索は全てのブロック(ListCard.vue)にかかるため、親コンポーネント(List.vue)に配置してpropsで子に渡しています。

ページ数やステータスは、それぞれブロックごとに個別の値を持つため、子コンポーネント側で変更したものをemitで親に渡しています。

人材募集

弊社グループでは一緒に働くメンバーを募集中です、ご応募お待ちしています!

7
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?