今回達成すること
検索結果に表示する記事を20件ずつ区切り、「もっと見る」ボタンで次の20件が表示する実装を行います。
最終的な検索ページのコード
また、検索結果が0件の場合にユーザーに表示するメッセージも作成します。
もっと見るボタンを実装する
現状の検索ページは、全ての記事が表示される状態です。
そこで取得したposts
配列を20件ずつ表示する実装を行います。
pages/search.vue
<template>
<v-container>
<ui-page-title
:title="pageTitle"
:sub-title="pageSubTitle"
/>
<!-- showPostsに書き換え -->
<ol>
<li
v-for="(post, i) in showPosts"
:key="`post-${i}`"
>
<nuxt-link
:to="$my.linkTo(post)"
>
{{ post.fields.title }}
</nuxt-link>
</li>
</ol>
<!-- 追加 -->
<v-btn
v-if="isShowMoreBtn"
block
large
:disabled="isDisableMoreBtn"
color="primary"
class="mt-8 text-capitalize"
@click="moreBtnOnClick"
>
Next {{ nextShowPostsCount }} Posts
</v-btn>
<!-- 削除 -->
<!-- <ol>
<li
v-for="(post, i) in posts"
:key="`post-${i}`"
>
{{ post.fields.title }}
</li>
</ol> -->
</v-container>
</template>
<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'
import { BlogPost } from '~/store/types'
@Component
export default class SearchPage extends Vue {
...
posts: BlogPost[] = []
// 追加
readonly showPostsLimit: number = 20
moreBtnClickCount: number = 0
...
get postContentType (): string {
return this.$accessor.post.content.type.post
}
// 以下、追加 ////////////////////////////////////////
// 20件ずつ増えるposts配列を返す
get showPosts (): BlogPost[] {
const showPostsCount: number =
(this.moreBtnClickCount + 1) * this.showPostsLimit
return this.posts.slice(0, showPostsCount)
}
/*
もっと見るボタンの表示・非表示フラグ
検索結果が20件を超える場合にtrueを返す
*/
get isShowMoreBtn (): boolean {
return this.posts.length > this.showPostsLimit
}
/*
もっと見るボタンの有効・無効フラグ
検索結果が表示投稿件数と一致した場合にtrueを返す
*/
get isDisableMoreBtn (): boolean {
return this.posts.length === this.showPosts.length
}
// 残りの投稿件数を表示する
get nextShowPostsCount (): number {
// 残りの全表示投稿数
const remainingShowPostsCount: number =
this.posts.length - this.showPosts.length
// 残りの表示投稿数が20件を超える場合 ? 20 : 残り表示投稿数
return (remainingShowPostsCount > this.showPostsLimit)
? this.showPostsLimit
: remainingShowPostsCount
}
// ここまで ////////////////////////////////////////
/*
Nuxt ssr & staticの挙動
asyncData()はgenerateコマンド時に実行され本番環境では稼働しない
そこでページ内で再実行可能なcreated()でAPIリクエストを行う
*/
async created (): Promise<void> {
// 検索APIリクエスト
await this.getContentful()
}
...
// 追加
// もっと見るボタンのクリック数のカウントアップを行う
moreBtnOnClick (): void {
this.moreBtnClickCount += 1
}
}
</script>
get showPosts()
... この算出プロパティがHTMLに表示するposts
配列となる。const showPostsCount
... ボタンクリック数 ×showPostsLimit
の値を返す。クリックのたびに20, 40, 60と増えていく。slice(<開始インデックス>, <終了インデックス>)
... 配列から指定の要素を取り出し、新しい配列を返すJavaScriptのメソッド。<終了インデックス>
の直前のインデックスまで取得する。(20を指定した場合 => インデックス19となる)
検索結果が20件を超える場合の挙動
Functionsサーバーを起動し、検索結果ページを確認しましょう。
検索結果が20件を超える場合、下記の挙動が確認できます。
- 配列が20件しか表示されていない
- もっと見るボタンが表示されている
- ボタンに「Next 20 Posts」のテキストが表示さている
もっと見るボタンをクリックすると、最後には残りの投稿件数が表示されます。
検索結果が20件を超えない場合の挙動
20件を超えない検索結果の場合は、もっと見るボタンは表示されません。
これで「もっと見る」ボタンの実装は完了です。
記事が見つからなかった場合の対応を行う
検索キーワードで記事が見つからなかった場合、ユーザーに再検索を促すメッセージを表示します。
Functionsへのリクエストは時間がかかるため、リクエストが完了した時にtrue
を返すisFinishedApiRequest
フラグを追加します。
pages/search.vue
@Component
export default class SearchPage extends Vue {
...
pageTitle: string = this.$my.routePageTitle(this.$route.name)
posts: BlogPost[] = []
readonly showPostsLimit: number = 20
moreBtnClickCount: number = 0
// 追加
isFinishedApiRequest: boolean = false
...
async created (): Promise<void> {
// 検索APIリクエスト
await this.getContentful()
// 追加
this.isFinishedApiRequest = true
}
...
}
これでリクエスト処理が完了したことを検知できるようになりました。
asyncメソッド内のawaitの挙動
async
メソッド内は、await
の処理が終了するまで次の処理に移りません。
完全にgetContentful()
の処理が終了した後に、isFinishedApiRequest
フラグをtrue
にしています。
検索結果が0件だった場合の分岐処理を行う
<template>
タグ内では、isFinishedApiRequest
フラグをv-if
に渡し、APIリクエストが完了した後にDOMを表示するように処理を行います。
pages/search.vue
<template>
<v-container>
<ui-page-title
:title="pageTitle"
:sub-title="pageSubTitle"
/>
<!-- APIリクエスト完了後に表示 -->
<template v-if="isFinishedApiRequest">
<!-- 検索結果 -->
<template v-if="posts.length">
<ol>
<li
v-for="(post, i) in showPosts"
:key="`post-${i}`"
>
<nuxt-link
:to="$my.linkTo(post)"
>
{{ post.fields.title }}
</nuxt-link>
</li>
</ol>
<v-btn
v-if="isShowMoreBtn"
block
large
:disabled="isDisableMoreBtn"
color="primary"
class="mt-8 text-capitalize"
@click="moreBtnOnClick"
>
Next {{ nextShowPostsCount }} Posts
</v-btn>
</template>
<!-- 検索結果が0の場合 -->
<template v-else>
<v-card
flat
color="transparent"
>
<v-card-subtitle
class="text-center"
>
記事が見つかりませんでした。<br>
他のキーワードやスペースを入れて検索してみてください。
</v-card-subtitle>
<v-card-actions
class="justify-center"
>
<ui-search-form />
</v-card-actions>
</v-card>
</template>
</template>
<!-- APIリクエスト中のコンテンツ -->
<template v-else>
<v-row
align="center"
justify="center"
:style="{ height: '50vh' }"
>
<v-progress-circular
size="40"
indeterminate
color="grey lighten-1"
/>
</v-row>
</template>
</v-container>
</template>
APIリクエスト中の表示
APIリクエスト中は、プログレスサークルが表示されます。
Vuetifyの<v-progress-circular>
を使用しています。
indeterminate
プロパティを渡すことでクルクル回るプログレスとなります。
記事が見つからなかった場合の表示
検索記事が見つからなかった場合は、検索のヒントが表示されます。
デプロイとコミット
最後に本番環境にデプロイし、検索ページが正しく動いているか確認しておきましょう。
% yarn netlify:deploy
以上で検索機能の実装を終わります。
マージし、GitHubにPushします。
% git commit -am "Finished implementation of search function"
% git checkout master
% git merge <ブランチ名>
$ git push
チャプターまとめ
このチャプターでは、Netlify Functionsを使った検索機能の実装と検索ページを作成しました。
- Netlify Functionsのセットアップ
- Functionsプロジェクトを本番環境にデプロイ
- Nuxt axiosモジュールのセットアップ
- FunctionsのCORS設定
- フォームのバリデーションを行う関数の作成
- ツールバーに検索フォームを作成する
- 検索結果を表示するページの作成
- FunctionsからContentfulへAPIリクエストを行う実装
- 検索結果ページの「もっと見る」ボタンの実装(今ここ)
次回は?
次回からブログアプリに必要なサイトマップの構築やマークダウンモジュールの導入を行います。
完成したsearch.vue
pages/search.vue
<template>
<v-container>
<ui-page-title
:title="pageTitle"
:sub-title="pageSubTitle"
/>
<!-- APIリクエスト完了後に表示 -->
<template v-if="isFinishedApiRequest">
<!-- 検索結果 -->
<template v-if="posts.length">
<ol>
<li
v-for="(post, i) in showPosts"
:key="`post-${i}`"
>
<nuxt-link
:to="$my.linkTo(post)"
>
{{ post.fields.title }}
</nuxt-link>
</li>
</ol>
<v-btn
v-if="isShowMoreBtn"
block
large
:disabled="isDisableMoreBtn"
color="primary"
class="mt-8 text-capitalize"
@click="moreBtnOnClick"
>
Next {{ nextShowPostsCount }} Posts
</v-btn>
</template>
<!-- 検索結果が0の場合 -->
<template v-else>
<v-card
flat
color="transparent"
>
<v-card-subtitle
class="text-center"
>
記事が見つかりませんでした。<br>
他のキーワードやスペースを入れて検索してみてください。
</v-card-subtitle>
<v-card-actions
class="justify-center"
>
<ui-search-form />
</v-card-actions>
</v-card>
</template>
</template>
<!-- APIリクエスト中のコンテンツ -->
<template v-else>
<v-row
align="center"
justify="center"
:style="{ height: '50vh' }"
>
<v-progress-circular
size="40"
indeterminate
color="grey lighten-1"
/>
</v-row>
</template>
</v-container>
</template>
<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'
import { BlogPost } from '~/store/types'
@Component
export default class SearchPage extends Vue {
layout (): string {
return 'app'
}
/*
returnの値が変化した場合にページコンポーネントを再レンダリングする
Doc: https://nuxtjs.org/docs/components-glossary/key
*/
key (route: { fullPath: string }): string {
return route.fullPath
}
pageTitle: string = this.$my.routePageTitle(this.$route.name)
posts: BlogPost[] = []
readonly showPostsLimit: number = 20
moreBtnClickCount: number = 0
isFinishedApiRequest: boolean = false
/*
string型 && 空白ではない場合、クエリの値を返す
'/search?q=%20'の場合、クエリは空白のstring型と判定されるので空白の場合はfalseを返すようtest()で判定
*/
get q (): false | string {
const q: string | (string | null)[] = this.$route.query.q
return (typeof q === 'string') && (!/^\s+$/.test(q)) && q
}
// 検索件数
get pageSubTitle (): string {
return (this.q)
? `${this.q} の検索結果 ${this.posts.length}件`
: '検索キーワードを入力してください'
}
// postのコンテンツタイプ名を返す
get postContentType (): string {
return this.$accessor.post.content.type.post
}
// 20件ずつ増えるposts配列を返す
get showPosts (): BlogPost[] {
const showPostsCount: number =
(this.moreBtnClickCount + 1) * this.showPostsLimit
return this.posts.slice(0, showPostsCount)
}
/*
もっと見るボタンの表示・非表示フラグ
検索結果が20件を超える場合にtrueを返す
*/
get isShowMoreBtn (): boolean {
return this.posts.length > this.showPostsLimit
}
/*
もっと見るボタンの有効・無効フラグ
検索結果が表示投稿件数と一致した場合にtrueを返す
*/
get isDisableMoreBtn (): boolean {
return this.posts.length === this.showPosts.length
}
// 残りの投稿件数を表示する
get nextShowPostsCount (): number {
// 残りの全表示投稿数
const remainingShowPostsCount: number =
this.posts.length - this.showPosts.length
// 残りの表示投稿数が20件を超える場合 ? 20 : 残り表示投稿数
return (remainingShowPostsCount > this.showPostsLimit)
? this.showPostsLimit
: remainingShowPostsCount
}
/*
Nuxt ssr & staticの挙動
asyncData()はgenerateコマンド時に実行され本番環境では稼働しない
そこでページ内で再実行可能なcreated()でAPIリクエストを行う
*/
async created (): Promise<void> {
// 検索APIリクエスト
await this.getContentful()
this.isFinishedApiRequest = true
}
/*
コンポーネントを描画するルートがナビゲーションから離れる直前に呼ばれる
thisでインスタンスにアクセス可能
Doc: https://router.vuejs.org/guide/advanced/navigation-guards.html#using-the-options-api
*/
beforeRouteLeave (_to: any, _from: any, next: any) {
// ページ遷移前にQueryを初期化する
this.$accessor.getSearchQuery('')
return next()
}
// Netlify FuncrionsからContentfulにAPIリクエストを行う
async getContentful (): Promise<void> {
// クエリが存在する場合
if (this.q) {
await this.$axios.$post(
'/.netlify/functions/search-contentful',
{
contentType: this.postContentType,
query: this.q
}
)
.then(({ items }: { items: BlogPost[] }) =>
(this.posts = items)
)
}
}
// もっと見るボタンのクリック数のカウントアップを行う
moreBtnOnClick (): void {
this.moreBtnClickCount += 1
}
}
</script>