検索ページに「もっと見る」ボタンを実装しよう【Nuxt.js×TypeScript】
  • 2022.02.19に公開
  • ブログ構築TS
  • 8. NetlifyFunctionsを使った検索機能
  • No.9 / 9

今回達成すること

検索結果に表示する記事を20件ずつ区切り、「もっと見る」ボタンで次の20件が表示する実装を行います。

最終的な検索ページのコード

2022-02-19 11-36-08

また、検索結果が0件の場合にユーザーに表示するメッセージも作成します。

2022-02-19 13-45-17

もっと見るボタンを実装する

現状の検索ページは、全ての記事が表示される状態です。

そこで取得した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」のテキストが表示さている

2022-02-19 11-36-08

もっと見るボタンをクリックすると、最後には残りの投稿件数が表示されます。

2022-02-19 11-43-25

検索結果が20件を超えない場合の挙動

20件を超えない検索結果の場合は、もっと見るボタンは表示されません。

2022-02-19 11-47-21

これで「もっと見る」ボタンの実装は完了です。

記事が見つからなかった場合の対応を行う

検索キーワードで記事が見つからなかった場合、ユーザーに再検索を促すメッセージを表示します。

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プロパティを渡すことでクルクル回るプログレスとなります。

2022-02-19 13-39-15

記事が見つからなかった場合の表示

検索記事が見つからなかった場合は、検索のヒントが表示されます。

2022-02-19 13-45-17

デプロイとコミット

最後に本番環境にデプロイし、検索ページが正しく動いているか確認しておきましょう。

% yarn netlify:deploy

以上で検索機能の実装を終わります。

マージし、GitHubにPushします。

% git commit -am "Finished implementation of search function"
% git checkout master
% git merge <ブランチ名>
$ git push

チャプターまとめ

このチャプターでは、Netlify Functionsを使った検索機能の実装と検索ページを作成しました。

  1. Netlify Functionsのセットアップ
  2. Functionsプロジェクトを本番環境にデプロイ
  3. Nuxt axiosモジュールのセットアップ
  4. FunctionsのCORS設定
  5. フォームのバリデーションを行う関数の作成
  6. ツールバーに検索フォームを作成する
  7. 検索結果を表示するページの作成
  8. FunctionsからContentfulへAPIリクエストを行う実装
  9. 検索結果ページの「もっと見る」ボタンの実装(今ここ)

次回は?

次回からブログアプリに必要なサイトマップの構築やマークダウンモジュールの導入を行います。

完成した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>
あなたの力になれること
私自身が独学でプログラミングを勉強してきたので、一人で学び続ける苦しみは痛いほど分かります。そこで、当時の私がこんなのあったら良いのにな、と思っていたサービスを立ち上げました。周りに質問できる人がいない、答えの調べ方が分からない、ここを聞きたいだけなのにスクールは高額すぎる。そんな方に向けた単発・短期間メンターサービスを行っています。
独学プログラマのサービス
ブログ構築TSの投稿
1
  • Nuxt.js×TypeScript開発環境構築
  • /
  • #01
Nuxt.jsをローカルPCに立ち上げよう
2
  • Nuxt.js×TypeScript開発環境構築
  • /
  • #02
Nuxt.jsプロジェクトをGitHubにPushしよう
3
  • Nuxt.js×TypeScript開発環境構築
  • /
  • #03
nuxt-property-decoratorのインストールとTypeScriptのセットアップ
1
  • Vuetifyセットアップ
  • /
  • #01
TypeScript環境のNuxt.jsにVuetifyを導入しよう
2
  • Vuetifyセットアップ
  • /
  • #02
VuetifyにカスタムCSSを追加してSASS変数を理解しよう
3
  • Vuetifyセットアップ
  • /
  • #03
VuetifyにカスタムSVGアイコンを追加しよう
1
  • NetlifyCLIを使ったNuxtデプロイ
  • /
  • #01
Netlify CLIをインストールして本番環境のサイトを作成しよう
2
  • NetlifyCLIを使ったNuxtデプロイ
  • /
  • #02
netlify.tomlを使ってNuxt.jsをNetlifyに手動デプロイしよう
1
  • Contentfulモデル構築
  • /
  • #01
Contentfulの料金とCommunityプランの無料枠を理解する
2
  • Contentfulモデル構築
  • /
  • #02
Contentfulへ新規会員登録、ロケールの変更、API Keyの発行を行う
3
  • Contentfulモデル構築
  • /
  • #03
Contentful ブログカテゴリーモデルを作成しよう
4
  • Contentfulモデル構築
  • /
  • #04
Contentful カテゴリーモデルに1対1で関連づくblogPostモデルを作成しよう
5
  • Contentfulモデル構築
  • /
  • #05
Contentful ブログ記事に1対多で関連づくplogTagモデルを作成しよう
6
  • Contentfulモデル構築
  • /
  • #06
Contentful カテゴリー・ブログ記事・タグコンテンツを作成しよう
1
  • Nuxt.js×Contentfulセットアップ
  • /
  • #01
Nuxt.js×Contentfulセットアップ。モジュールのインストールからAPI Keyの登録まで
2
  • Nuxt.js×Contentfulセットアップ
  • /
  • #02
Contentful APIリクエストの実行 Nuxt.jsにブログコンテンツを表示しよう
3
  • Nuxt.js×Contentfulセットアップ
  • /
  • #03
ContentfulAPIをNetlifyにデプロイしよう【Nuxt FullStaticのasyncDataとfetch】
1
  • Vuex×TypeScriptセットアップ
  • /
  • #01
Vuexの型付け vuex-module-decoratorsとnuxt-typed-vuexどちらを使用するか
2
  • Vuex×TypeScriptセットアップ
  • /
  • #02
nuxt-typed-vuexのインストールとセットアップ。Vuexの型定義と呼び出し方
3
  • Vuex×TypeScriptセットアップ
  • /
  • #03
VuexにContentfulの型定義ファイルとnuxtServerInitを追加しよう
4
  • Vuex×TypeScriptセットアップ
  • /
  • #04
VuexにContentfulAPIレスポンスを保存してVueファイルに表示しよう
1
  • コンテンツページ構築
  • /
  • #01
ブログアプリのページ設計とNuxt.jsの動的ルーティングについて理解しよう
2
  • コンテンツページ構築
  • /
  • #02
カテゴリーのコンテンツページを作成しよう【Nuxt.js×Contentful】
3
  • コンテンツページ構築
  • /
  • #03
カテゴリーに関連付くブログ記事一覧を表示しよう【Nuxt.js×Contentful】
4
  • コンテンツページ構築
  • /
  • #04
injectを使用して共通エラー処理メソッドを作成しよう【Nuxt×TypeScript】
5
  • コンテンツページ構築
  • /
  • #05
NuxtChildを使用してブログ記事ページを作成しよう【Nuxt.js×TypeScript】
6
  • コンテンツページ構築
  • /
  • #06
タグ一覧ページとタグ関連づく記事一覧を表示しよう【Nuxt.js×TypeScript】
7
  • コンテンツページ構築
  • /
  • #07
プライバシーポリシーページを作成しよう【Nuxt.js×TypeScript】
8
  • コンテンツページ構築
  • /
  • #08
@nuxtjs/i18nのインストールとセットアップ。ページタイトルの翻訳化【TypeScript】
1
  • NetlifyFunctionsを使った検索機能
  • /
  • #01
Netlify Functionsを使ってクエリを返す関数を作成しよう【Nuxt.js×TypeScript】
2
  • NetlifyFunctionsを使った検索機能
  • /
  • #02
Netlify Functionsプロジェクトをデプロイしよう【Nuxt.js×TypeScript】
3
  • NetlifyFunctionsを使った検索機能
  • /
  • #03
Nuxt.js × axiosセットアップ Netlify Functionsにリクエストを行う準備をしよう
4
  • NetlifyFunctionsを使った検索機能
  • /
  • #04
オリジン•CORS•プリフライトリクエストを理解する【Nuxt.js×Netlify Functions】
5
  • NetlifyFunctionsを使った検索機能
  • /
  • #05
Netlify Functionsを使ってフォームバリデーション機能を構築しよう【Nuxt.js】
6
  • NetlifyFunctionsを使った検索機能
  • /
  • #06
ツールバーに表示する検索フォームを作成しよう【Nuxt.js×TypeScript】
7
  • NetlifyFunctionsを使った検索機能
  • /
  • #07
検索ページを作成しよう【Vue propsとTypeScriptの書き方 解説】
8
  • NetlifyFunctionsを使った検索機能
  • /
  • #08
Netlify FunctionsからContentfulAPIリクエストを送ろう【Nuxt.js】
9
  • NetlifyFunctionsを使った検索機能
  • /
  • #09
検索ページに「もっと見る」ボタンを実装しよう【Nuxt.js×TypeScript】
1
  • ブログMarkdown対応
  • /
  • #01
@nuxtjs/markdownitのインストールとセットアップ【Nuxt.js×TypeScript】
2
  • ブログMarkdown対応
  • /
  • #02
Nuxt.js×markdown-it 外部リンクを別タブで開くプラグインを追加しよう
3
  • ブログMarkdown対応
  • /
  • #03
Nuxt.js×markdown-it 内部リンクをVueRouterで高速にページ遷移しよう
4
  • ブログMarkdown対応
  • /
  • #04
Nuxt.js×markdown-it アンカーリンクとブログ目次を自動生成しよう
独学プログラマ
独学でも、ここまでできるってよ。
CONTACT
Nuxt.js制作のご依頼は下記メールアドレスまでお送りください。