2
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?

More than 3 years have passed since last update.

【Rails6 + Vue.js】ブックマークを管理する簡単なSPAを作ってみた話(第三章:検索・絞り込み機能)

Last updated at Posted at 2021-04-04

前回の記事の続きになります。
前回の記事はこちら↓

本記事の概要

完成版のイメージ

今回実装するフリーワード検索、カテゴリー別絞り込み機能の完成イメージです。

Image from Gyazo

今回実装する機能について

【フリーワード検索機能】

  • フォームに文字を入力すると対象の文字に一致するタイトル、カテゴリーを検索し、ヒットしたブックマークを非同期で表示します。

【カテゴリー別絞り込み機能】

  • カテゴリーを選択するとそのカテゴリーで登録されているブックマークを表示します。
  • 新しいカテゴリーが追加された場合は自動的に選択肢にも追加されます。
  • フリーワード検索機能との併用も可能にします。

【登録したURLへのアクセス機能】
前回までの記事で実装していなかったため今回の記事で実装します。

  • 新規投稿時に登録したURLへ新規タブでアクセスできるようにします。
  • 新規タブを開いてリンクする際のセキュリティ上の脆弱性についての対策も施します。

記事構成

本アプリケーションについての記事は大きく4部構成で作成しております。

本記事の参考URL

本記事の作成にあたって主に以下のドキュメント・記事を参考にさせて頂きました。

実装

それでは実装に移ります。

検索機能

templateに検索フォームを用意

app/javascript/app.vue
<template>
  <v-app id="app">
  <!-- 中略 -->
    <h3>フリーワードで探す</h3>
      <v-text-field v-model="searchWord" @keyup="searchBookmarks" label="Input Keyword" style='margin-top:4px'></v-text-field>
      <br>
      <h3>カテゴリーごとに絞る</h3>
      <v-select
        v-model='category'
        :items="categories"
        label="Category"
        @change="searchBookmarks">
      </v-select>
  <!-- 中略 -->
  </v-app>
</template>

Vue.jsで検索機能を実装

今回は indexOf()メソッド を使って実装します。
indexOf() メソッドは引数に与えられた内容と同じ内容を持つ最初の配列要素の添字を返し、存在しない場合は -1 を返します。

つまり、上の「フリーワードで探す」のフォームで入力した文字はv-model="searchWord"searchWordに入り、そのsearchWordが全てのブックマークの配列の中でタイトルやカテゴリーにヒットした場合、最初のキーを返し、1つもヒットしない場合は戻り値が-1になるということになります。

参考↓

app/javascript/app.vue
<script>
import draggable from 'vuedraggable'
import axios from 'axios';

axios.defaults.headers.common = {
    'X-Requested-With': 'XMLHttpRequest',
    'X-CSRF-TOKEN' : document.querySelector('meta[name="csrf-token"]').getAttribute('content')
};

export default {
  data: function () {
    return {
      isLoading: true,
      bookmarkList: ['',''],
      allData: ['',''],
      categories: ['All'],
      categoriesForEdit: [],
      category: 'ALL',
      dialogPostFlag: false,
      postTitle: "",
      postUrl: "",
      postCategory: "",
      dialogPutFlag: false,
      putTitle: '',
      putUrl: '',
      putCategory: '',
      dialogDeleteFlag: false,

      searchWord: '',  // 追加
    }
  },
  ...
  methods: {
    setBookmark: function () {
      axios.get('/api/bookmarks')
      .then(response => {
        this.allData = response.data
        this.bookmarkList = this.allData
        }
      );
      this.listCategories();
      this.searchBookmarks();  //searchBookmarksを呼ぶ
    },
    listCategories: function() {
      this.categories = []
      this.categoriesForEdit = []
      this.categories.push('ALL')
      for (i=0; i<this.allData.length; i++) {
        if (this.categories.indexOf(this.allData[i].category) == -1) {
          this.categories.push(this.allData[i].category)
          this.categoriesForEdit.push(this.allData[i].category)
        }
      }
    },

    ...

    // 検索するメソッド
    searchBookmarks: function() {
      if (this.category == 'ALL') {  // カテゴリーの選択肢が'ALL'(初期値)だったら
        this.bookmarkList = []
        for (i=0; i<this.allData.length; i++) {
          // 入力した文字(searchWord)がブックマークのタイトルまたはカテゴリーと一致する場合
          if ((this.allData[i].title.indexOf(this.searchWord) !== -1) || (this.allData[i].category.indexOf(this.searchWord) !== -1)) {
            this.bookmarkList.push(this.allData[i])  // 結果を返して配列にpush
          }
        }
      } else if (this.category != '') {  // カテゴリーが選択されている場合
        this.bookmarkList = []
        for (i=0; i<this.allData.length; i++) {
          // 選択したカテゴリーとブックマークのカテゴリーが等しい場合
          if (this.allData[i].category == this.category) {
            // 入力した文字(searchWord)がブックマークのタイトルまたはカテゴリーと一致する場合
            if ((this.allData[i].title.indexOf(this.searchWord) !== -1) || (this.allData[i].category.indexOf(this.searchWord) !== -1)) {
              this.bookmarkList.push(this.allData[i])  // 結果を返して配列にpush
            }
          }
        }   
      }
    }
  }
}
</script>

【フリーワード検索機能について】

  • searchBookmarksメソッドでは、indexOf() を使って渡されてきたsearchWordが、全ブックマークの中のタイトルやカテゴリーと一致する場合(戻り値が -1 では無い場合)に検索結果となるブックマークをbookmarkListの配列の中に随時pushする形で実装しています。

  • indexOf()は、前回の記事のlistCategoriesメソッドを定義した時に既に使用していますが、今回改めて自分の中での学習したことをアウトプットとして文章化しました。

  • listCategoriesメソッドではカテゴリーが既存の物と一致しない場合(戻り値が -1 である場合)にそのカテゴリーを配列の中に新たにpushするというものでした。

【カテゴリー別絞り込み機能について】

  • カテゴリー別絞り込み機能では、テンプレートのv-select内の:items="categories"で全てのカテゴリーを表示し、v-model='category'で選択したカテゴリーを渡してsearchBookmarksメソッドを呼びます。

  • searchBookmarksメソッドの else if の部分で処理が実行され結果を配列の中にpushし表示されます。

以上で検索機能の実装は終了です。

登録したURLへのアクセス機能

前回までの実装では、表示されているブックマークのタイトルは、ただの文字列で表示していました。

なのでブックマーク管理アプリとして機能するように、タイトルをクリックすると新規タブを開き登録したURLでアクセスできるように実装していきます。

app/javascript/app.vue
<template>
  <v-app id="app">
  <!-- 中略 -->
    <a v-bind:href="bookmark.url" target="_blank" rel="noopener noreferrer" style="font-size: 18px;">
      {{ bookmark.title }}
    </a>
  <!-- 中略 -->
  </v-app>
</template>

タイトルを aタグで囲み、href属性で登録したブックマークのURLを指定します。
しかし、そのまま<a href="{{ bookmark.url }}">としてしまうと機能しません。

公式を見ると、「Mustache は、HTML 属性の内部で使用することはできません。代わりに、v-bind ディレクティブを使用してください」 と書いてありました。

なので、<a v-bind:href="bookmark.url">という形で実装しました。

参考URL:公式↓

また、新規タブを開いてアクセスできるようにtarget="_blank"を指定します。しかしそのままだとフィッシング詐欺攻撃の可能性などの危険性があるため、rel="noopener noreferrer"を付与して防ぎます。

参考にさせていただいた記事:

以上で、タイトルをクリックすると新規タブを開いて登録したURLにアクセスできるようになりました。

次章、ローディング表示、ページネーション、ユーザー管理機能の実装の記事はこちらになります↓

2
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
2
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?