TECH::EXPERT【42日目】

【学習内容】


moooviでインクリメンタルサーチを実装し、映画の検索を非同期で行えるようにする

【なにを目指すか】


映画検索画面にインクリメンタルサーチを実装して、文字が入力されるごとに検索をかけ結果を表示するようにします。


【事前準備】インデックスでデータの検索を高速化する

テーブル内で頻繁に検索が行われるカラムには、インデックスを設定すると処理を高速化できます。

検索窓では、映画のタイトルを検索するので、「productテーブルのtitleカラム」にインデックスを貼ります。

・マイグレーション作成

$rails g migration AddIndexToProducts

migrationファイル

class AddIndexToProducts < ActiveRecord::Migration
 def change
   add_index :products,  :title
 end
end
$bundle exec rake db:create

・application.jsの記述を確認する

application.jsに//= require jquery //= require rails-ujsの記載がされているか確認します。

【インクリメンタルサーチ実装のステップ】

後半はPictweetの非同期通信とだいたい一緒ですが、前半部分は大きく異なります。

①ルーティングなどAPI側の準備をする
②テキストフィールドを作成する
③テキストフィールドに入力されるたびにイベントが発火するようにする
④イベント時に非同期通信できるようにする
⑤非同期通信の結果を得て、HTMLを作成する
⑥エラー時の処理を行う

①ルーティングなどAPI側の準備をする

・検索におけるルーティングは既にできているので、アクションの中での条件分岐を定義します。

app/controlers/products_controller.rb

class ProductsController < RankingController

before_action :authenticate_user!, only: :search

def index
  @products = Product.order('id ASC').limit(20)
end

def show
  @product = Product.find(params[:id])
end

def search
  @products = Product.where('title LIKE(?)', "%#{params[:keyword]}%").limit(20)
  respond_to do |format|
    format.html
    format.json
  end
end
end

また、映画情報を取得したら、jbuilderを使ってJavaScript側に返します。

・「search.json.jbuilder」ファイルを作成する。
App > views > products > search.json.jbuilder

・「search.json.jbuilder」ファイルを編集する。

app/views/products/search.json.jbuilder

json.array! @products do |product|
 json.id product.id
 json.title product.title
 json.image product.image_url
 json.detail product.detail
end

今回のように結果が複数個得られる場合は、JSON形式のデータを配列で返します。

その場合は、上記のようにarray!を使用します。

②テキストフィールドを作成する
すでにmooovi内に検索窓が設置されているので、作業不要です。

③テキストフィールドに入力されるたびにイベントが発火するようにする
テキストフィールドに文字が入力されるたびにイベントが呼び出させるようにします。
(イメージ的にはGoogleの検索窓みたいに候補がずらずらと出てくる感じです)

・「search.js」を作成する

App > aseets > javascripts > search.js

・「search.js」を編集する

クラス名が".search__query”の部分のテキストフィールドがkeyupしたら、テキストフィールドの文字を取得して変数inputに代入します。

app/assets/javascripts/search.js

$(function() {
 $(".search__query").on("keyup", function() {
   var input = $(".search__query").val();
 });
});

④イベント時に非同期通信できるようにする

キーが入力される度に非同期通信で映画タイトルを検索できるようにします。
HTTPメソッドはGET、/products/searchのURLに{ keyword: input }を送信、
サーバから値を返す際は、jsonです。

Pictweetのときは、var url = $(this).attr('action')と定義していたため、#new_commentの値をvar urlに代入していました。

しかし、今回は上記のリクエストによって、products_controllerのsearchアクションが動くようになります。

$(function() {
 $(".search__query").on("keyup", function() {
   var input = $(".search__query").val();
   $.ajax({
     type: 'GET',
     url: '/products/search',
     data: { keyword: input },
     dataType: 'json'
   })
 });
});

app/controlers/products_controller.rb

def search
  @products = Product.where('title LIKE(?)', "%#{params[:keyword]}%").limit(20)
  respond_to do |format|
    format.html
    format.json
  end
end

うまくいっている場合は、該当する映画情報はjbuilderによってJSONに変換されてjavascriptのファイルに返されます。


⑤非同期通信の結果を得て、HTMLを作成する

非同期通信の結果をdoneの関数の引数から受取り、ビューに追加します。

$(".listview.js-lazy-load-images").は、検索に一致しないほかの映画情報のことです。
また、empty()は子要素のみを削除するメソッドです。

productsが空ではない場合は、forEachメソッドを用いて、productsの中身の数だけappendProduct関数を呼び出します

appendProductとappendErrMsgToHTMLはこの次に定義します。

app/assets/javascripts/search.js

$(function() {
 $(".search__query").on("keyup", function() {
   var input = $(".search__query").val();
   $.ajax({
     type: 'GET',
     url: '/products/search',
     data: { keyword: input },
     dataType: 'json'
   })
  .done(function(products) {
    $(".listview.js-lazy-load-images").empty();
    if (products.length !== 0) {
      products.forEach(function(product){
        appendProduct(product);
      });
    }
    else {
      appendErrMsgToHTML("一致する映画はありません");
    }
  })
 });
});

app/assets/javascripts/search.js

$(function() {
function appendProduct(product) {
  var html = `<li>
                 <a class="listview__element--right-icon" href="/products/${ product.id }/reviews/new" title="${ product.title }">
                   <div class="position-right p1em">
                     <i class="icon-chevron-right color-sub"></i>
                   </div>
                   <div class="row no-space-bottom">
                     <div class="col2">
                       <div class="thumbnail thumbnail--movies">
                         <div class="thumbnail__figure" style="background-image: url(${ product.image });" title="${ product.title }"></div>
                       </div>
                     </div>
                     <div class="col6 push6">
                       <h3 class="text-middle text-break">
                         <span class="color-sub">${ product.title }</span>
                       </h3>
                       <p class="text-xsmall text-overflow">
                         ${ product.detail }
                       </p>
                     </div>
                   </div>
                 </a>
               </li>`
 }
 $(".search__query").on("keyup", function() {
   var input = $(".search__query").val();
   $.ajax({
     type: 'GET',
     url: '/products/search',
     data: { keyword: input },
     dataType: 'json'
   })
   .done(function(products) {
     $(".listview.js-lazy-load-images").empty();
     if (products.length !== 0) {
       products.forEach(function(product){
         appendProduct(product);
       });
     }
     else {
       appendErrMsgToHTML("一致する映画はありません");
     }
   })
   .fail(function() {
     alert('error');
   });
 });
});


・search.jsを編集する
app/assets/javascripts/search.js

$(function() {
var search_list = $(".listview.js-lazy-load-images");
function appendProduct(product) {
  var html = `<li>
                 <a class="listview__element--right-icon" href="/products/${ product.id }/reviews/new" title="${ product.title }">
                   <div class="position-right p1em">
                     <i class="icon-chevron-right color-sub"></i>
                   </div>
                   <div class="row no-space-bottom">
                     <div class="col2">
                       <div class="thumbnail thumbnail--movies">
                         <div class="thumbnail__figure" style="background-image: url(${ product.image });" title="${ product.title }"></div>
                       </div>
                     </div>
                     <div class="col6 push6">
                       <h3 class="text-middle text-break">
                         <span class="color-sub">${ product.title }</span>
                       </h3>
                       <p class="text-xsmall text-overflow">
                         ${ product.detail }
                       </p>
                     </div>
                   </div>
                 </a>
               </li>`
   search_list.append(html);
}
function appendErrMsgToHTML(msg) {
   var html = `<li>
                 <div class='listview__element--right-icon'>${ msg }</div>
               </li>`
   search_list.append(html);
 }
 $(".search__query").on("keyup", function() {
   var input = $(".search__query").val();
   $.ajax({
     type: 'GET',
     url: '/products/search',
     data: { keyword: input },
     dataType: 'json'
   })
   .done(function(products) {
     $(".listview.js-lazy-load-images").empty();
     if (products.length !== 0) {
       products.forEach(function(product){
         appendProduct(product);
       });
     }
     else {
       appendErrMsgToHTML("一致する映画はありません");
     }
   })
 });
});

⑥エラー時の処理を行う

通信に失敗した場合(failのとき)の処理として、アラートで「映画検索に失敗しました」と表示させます。

$(function() {
var search_list = $(".listview.js-lazy-load-images");
function appendProduct(product) {
  var html = `<li>
                 <a class="listview__element--right-icon" href="/products/${ product.id }/reviews/new" title="${ product.title }">
                   <div class="position-right p1em">
                     <i class="icon-chevron-right color-sub"></i>
                   </div>
                   <div class="row no-space-bottom">
                     <div class="col2">
                       <div class="thumbnail thumbnail--movies">
                         <div class="thumbnail__figure" style="background-image: url(${ product.image });" title="${ product.title }"></div>
                       </div>
                     </div>
                     <div class="col6 push6">
                       <h3 class="text-middle text-break">
                         <span class="color-sub">${ product.title }</span>
                       </h3>
                       <p class="text-xsmall text-overflow">
                         ${ product.detail }
                       </p>
                     </div>
                   </div>
                 </a>
               </li>`
   search_list.append(html);
}
function appendErrMsgToHTML(msg) {
   var html = `<li>
                 <div class='listview__element--right-icon'>${ msg }</div>
               </li>`
   search_list.append(html);
 }
 $(".search__query").on("keyup", function() {
   var input = $(".search__query").val();
   $.ajax({
     type: 'GET',
     url: '/products/search',
     data: { keyword: input },
     dataType: 'json'
   })
   .done(function(products) {
     $(".listview.js-lazy-load-images").empty();
     if (products.length !== 0) {
       products.forEach(function(product){
         appendProduct(product);
       });
     }
     else {
       appendErrMsgToHTML("一致する映画はありません");
     }
   })
   .fail(function() {
     alert('映画検索に失敗しました');
   })
 });
});

【所感】

基本的に先日のnoteとやっていることは一緒でした。

インクリメンタルサーチについて大枠はしっかりと捉えることはできましたが、細かいコードについてはまだまだ知識が不足しています。

Chat-spaceに機能実装する段階で調べながら手を動かして身につけたいと思います。

さて、次回はChat-space理解度テスト(HTML)です。

なんとなく描画はできましたが、正直まだあやしい部分もあるので、しっかり予習・復習を進めていきます。

この記事が気に入ったらサポートをしてみませんか?