これは、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・・・リリースの配信先が追加できるかどうか
以下に実装例を示します。ここでは説明に不要な部分は省略しています。
親コンポーネント
<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>
子コンポーネント
<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で親に渡しています。
人材募集
弊社グループでは一緒に働くメンバーを募集中です、ご応募お待ちしています!