hiyoko-programingの日記

プログラミングを勉強したてのひよっ子。   エンジニア目指して勉強中。

インクリメンタルサーチ

投稿の検索をインクリメンタルサーチで検索できるようにする。

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

https://tech-master.s3.amazonaws.com/uploads/curriculums//68835811e50d3c062c47b7b7a338a6ca.gif

事前準備

Pictweetの投稿検索をインクリメンタルサーチで行えるように準備を行う。

必ず以下から新しいインクリメンタルサーチ実装用のPictweetをgit cloneをしてから、インクリメンタルサーチを実装。

 実装用に、新しいPictweetをgit cloneする

ターミナル
1
2
3
4
5
6
$ cd ~/projects
$ git clone https://github.com/exp-drill/search_pictweet.git
$ cd search_pictweet
$ bundle install
$ rails db:create
$ rails db:migrate
bundle installでエラーが生じた場合は以下のコマンドを実行し、再度bundle installを実行する
ターミナル
1
$ bundle config --global build.mysql2 --with-opt-dir="$(brew --prefix openssl)"

インデックスでデータの検索を高速する

tweetsテーブルのtextカラムにインデックスを貼ることで、データの検索を高速化する。

 textカラムに対するインデックスを設定

まずは、tweetsテーブルに対してインデックスを貼るためのマイグレーションファイルを作成する。

ターミナル
1
$ rails g migration AddIndexToTweets

作成したマイグレーションファイルを編集してtextカラムにインデックスを貼る。

migrationファイル
1
2
3
4
5
class AddIndexToTweets < ActiveRecord::Migration
  def change
    add_index :tweets, :text, length: 32
  end
end

記述ができたらターミナルでマイグレーションを実行。

問題なく実行できたtweetsテーブルのtextカラムに対してインデックスが設定できている。

 application.jsの記述を確認

Pictweetにおけるコメント投稿の非同期通信化を行ったときと同様に、確認する。

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

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

1. ルーティングなどAPI側の準備

アクションの中でHTMLとJSONなどのフォーマット毎に条件分岐する記述を追加する。
フォーマット毎に処理を分けるには、respond_toを使用する。

 respond_toを利用してフォーマット毎に処理を分ける

app/controlers/tweets_controller.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class TweetsController < ApplicationController
  before_action :set_tweet, except: [:index, :new, :create, :search]
  before_action :move_to_index, except: [:index, :show, :search]

  def index
    @tweets = Tweet.includes(:user).order("created_at DESC").page(params[:page]).per(5)
  end

  # 中略

  def search
    @tweets = Tweet.search(params[:keyword])
    respond_to do |format|
      format.html
      format.json
    end
  end

  # 以下省略

投稿情報を取得したら、jbuilderを使ってJavaScript側に返す。

検索結果は、複数の投稿情報を表示させる。
そのため、複数の投稿情報が格納された配列を返すようなjbuilderの記述にする必要がある。

 「search.json.jbuilder」というファイルを以下のディレクトリに作成

app/views/tweets/search.json.jbuilder
1
2
3
4
5
6
7
8
json.array! @tweets do |tweet|
  json.id tweet.id
  json.text tweet.text
  json.image tweet.image
  json.user_id tweet.user_id
  json.nickname tweet.user.nickname
  json.user_sign_in current_user
end

JSON形式のデータを配列で返したい場合は、上記のようにarray!を使用。

 jbuilder:array! メソッド

jbuilderという拡張子を持つテンプレートでは、JSONという名前のJbuilderオブジェクトが自動的に利用できるようになる。
Jbuilderオブジェクトは、JSON形式に返すための便利なメソッドがたくさん用意されており、配列で返したい場合はarray!を使用する。

array!を使用することで以下のように、JavaScript側に配列で値を送ることが可能。

JavaScript側に送る配列
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[{
 id: 1,
 image: "https://~.jpg",
 nickname: "やべ",
 text: "プログラミングの勉強中",
 user_id: 1,
 user_sign_in:
    {created_at: "2019-10-08T01:47:37.000Z",
    email: "aaa@gmail.com",
    id: 1,
    nickname: "やべ",
    updated_at: "2019-10-08T01:47:37.000Z"}
}]

jbuilderを使用するとより少ない記述でJSON形式のデータを作ることができる。

2. テキストフィールドを作成

今回はすでに検索を入力するためのテキストフィールドを作成してあるので、こちらの作業は必要ない。

3. テキストフィールドが入力されるたびにイベントが発火するようにする

テキストフィールドに文字が入力されるたびにイベントが呼び出されるように実装する。
文字打ち込み終わったら、つまりキーを離したら処理をさせたいときはkeyupメソッドを使用する。keyupメソッドを使って文字を入力する度にイベントが発火するか確かめてみる。

 search.jsというファイルを以下のディレクトリに作成

  •  app
    •  assets
      •  javascripts
        •  search.js
app/assets/javascripts/search.js
1
2
3
4
5
$(function() {
  $(".search-input").on("keyup", function() {
    var input = $(".search-input").val();
  });
});

上記のコードにおいて

今回の実装で使用するテキストフィールドは以下の部分。

app/views/tweets/index.html.erb
1
2
3
4
<%= form_with(url: search_tweets_path, local: true, method: :get, class: "search-form") do |form| %>
  <%= form.text_field :keyword, placeholder: "投稿を検索する", class: "search-input" %>
  <%= form.submit "検索", class: "search-btn" %>
<% end %>

テキストフィールドのclass名はsearch-inputである。

よって、

app/assets/javascripts/search.js
1
2
3
  $(".search-input").on("keyup", function() {
    var input = $(".search-input").val();
  });

の記述は、
「クラス名が".search-input”の部分のテキストフィールドがkeyupしたら、テキストフィールドの文字を取得して変数inputに代入する」
ということを表している。

フォームの値を取得するときはval()を使う。

それでは、console.log()を使い、フォームの値を確認。

app/assets/javascripts/search.js
1
2
3
4
5
6
$(function() {
  $(".search-input").on("keyup", function() {
    var input = $(".search-input").val();
    console.log(input);
  });
});

編集ができたら、検索の画面でコンソールを開き、フォームに入力した値が出力されるか確認。

https://tech-master.s3.amazonaws.com/uploads/curriculums//f2b6fc35e4c6bce76427eddde968f96c.gif

コンソールに入力した値が出力されていれば成功。

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

キーが入力される度に非同期通信で投稿を検索できるように実装する。
1で設定したルーティングにアクセスして、コントローラーで該当する投稿内容を検索。

app/assets/javascripts/search.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$(function() {
  $(".search-input").on("keyup", function() {
    var input = $(".search-input").val();
    $.ajax({
      type: 'GET',
      url: '/tweets/search',
      data: { keyword: input },
      dataType: 'json'
    })
  });
});

Ajax通信を実現するためには、上記のように$.ajaxメソッドを使用する。

上記のコードは以下のような意味になる。
HTTPメソッドはGETで、/tweets/searchのURLに{ keyword: input }を送信。サーバーから値を返す際は、JSONになる。

rails routesで確かめれば明らかだが、上記のリクエストによって、tweets_controller.rbのsearchアクションが動く。

ここで、searchアクションのコードを確認。

app/controllers/tweets_controller.rb
1
2
3
4
5
6
7
  def search
    @tweets = Tweet.search(params[:keyword])
    respond_to do |format|
      format.html
      format.json
    end
  end

$.ajaxのdataTypeでJSONを指定しているので、サーバーはJSON形式で値を返す。
普段のレスポンス(html形式のレスポンス)ではtweets_controller.rbのsearchアクションが実行されたら、

app/views/tweets/search.html.erbが読まれるが、

JSON形式の場は、app/views/tweets/search.json.jbuilderが読まれる。

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

5. 非同期通信の結果を得て、HTMLを作成

非同期通信の結果をdoneの関数の引数から受取り、ビューに追加するためのHTMLを作成する必要がある。

まずは、doneの中身でどんな値を取得をできるかデバッグする。

app/assets/javascripts/search.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$(function() {
  $(".search-input").on("keyup", function() {
    var input = $(".search-input").val();
    $.ajax({
      type: 'GET',
      url: '/tweets/search',
      data: { keyword: input },
      dataType: 'json'
    })
    .done(function(tweets) {
      console.log(tweets);
    })
  });
});

サーバーエラーが起きていないときは、jbuilderからdoneの関数に投稿情報が返ってくる。
今回jbuilderarray!を使用しているため、配列型で情報が返ってきていて、その情報がtweetsに代入されている。

console.log()でtweetsの中身をブラウザで確認すると、以下の画像のように配列でjbuilderからの情報を受け取れていることが確認できる。

https://tech-master.s3.amazonaws.com/uploads/curriculums//3e3d77fab602f10cc4929b453501ce7a.png

tweetsの中身が確認できたら、console.log()は削除して、doneの関数の中身を記述する。

app/assets/javascripts/search.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
$(function() {
  $(".search-input").on("keyup", function() {
    var input = $(".search-input").val();
    $.ajax({
      type: 'GET',
      url: '/tweets/search',
      data: { keyword: input },
      dataType: 'json'
    })
    .done(function(tweets) {
      $(".contents.row").empty();
      if (tweets.length !== 0) {
        tweets.forEach(function(tweet){
          appendTweet(tweet);
        });
      }
      else {
        appendErrMsgToHTML("一致するツイートがありません");
      }
    })
  });
});

 

上記のコードについて

app/assets/javascripts/search.js
1
2
3
   .done(function(tweets) {
      $(".contents.row").empty();
   })

インクリメンタルサーチでは、検索をする直前に投稿情報のリストを削除してあげる必要がある。

https://tech-master.s3.amazonaws.com/uploads/curriculums//538409f89e2c4762d6458838b43fd2d5.png

上の画像の部分を表示するビューを確認する。

app/views/tweets/index.html.erb
1
2
3
4
5
6
<div class="contents row">
  <% @tweets.each do |tweet| %>
    <%= render partial: "tweet", locals: { tweet: tweet } %>
  <% end %>
  <%= paginate(@tweets) %>
</div>

上記コードの検索結果欄で出力されている投稿情報をemptyメソッドを使用して削除。

 empty メソッド

指定したDOM要素の子要素のみを削除するメソッド。
指定したDOM要素自体を削除するremoveメソッドとは異なるので注意。

投稿情報をすべて削除したいので、<div class="contents row">の要素を取得すればいい。

class名は、.contents.rowとする。

app/assets/javascripts/search.js
1
$(".contents.row").empty();

よって、上記のコードで投稿の情報を削除できる。

次に、jbuilderから送られてきた配列の情報によって、場合分けをし関数を呼び出す。

app/assets/javascripts/search.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  .done(function(tweets) {
    $(".contents.row").empty();
    if (tweets.length !== 0) {
      tweets.forEach(function(tweet){
        appendTweet(tweet);
      });
    }
    else {
      appendErrMsgToHTML("一致するツイートがありません");
    }
  })
 appendTweetとappendErrMsgToHTMLは後で定義する。

これらの関数は、jbuilderから得られた値を投稿情報のリストに追加するものである。

tweetsが空ではない場合(tweets.length !== 0)

forEachメソッドを用いて、tweetsの中身の数だけappendTweet関数を呼び出す。

 forEach メソッド

forEachは、与えられた関数を配列に含まれる各要素に対して一度ずつ呼び出す。

tweetsが空の場合

”一致するツイートがありません”という引数を与え、appendErrMsgToHTML関数を呼び出す。

tweetsに投稿の情報が入っている場合のappendTweet関数を定義する。

app/assets/javascripts/search.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
$(function() {

  var search_list = $(".contents.row");

  function appendTweet(tweet) {
    if(tweet.user_sign_in && tweet.user_sign_in.id == tweet.user_id){
      var current_user = `<li>
                            <a href="/tweets/${tweet.id}/edit" data-method="get" >編集</a>
                          </li>
                          <li>
                            <a href="/tweets/${tweet.id}" data-method="delete" >削除</a>
                          </li>`
    } else {
      var current_user = ""
    }

    var html = `<div class="content_post" style="background-image: url(${tweet.image});">
                  <div class="more">
                    <span><img src="/assets/arrow_top.png"></span>
                    <ul class="more_list">
                      <li>
                        <a href="/tweets/${tweet.id}" data-method="get" >詳細</a>
                      </li>
                      ${current_user}
                    </ul>
                  </div>
                  <p>${tweet.text}</p><br>
                  <span class="name">
                    <a href="/users/${tweet.user_id}">
                      <span>投稿者</span>${tweet.nickname}
                    </a>
                  </span>
                </div>`
    search_list.append(html);
  }

  $(".search-input").on("keyup", function() {
    var input = $(".search-input").val();
    $.ajax({
      type: 'GET',
      url: '/tweets/search',
      data: { keyword: input },
      dataType: 'json'
    })
    .done(function(tweets) {
      search_list.empty();
      if (tweets.length !== 0) {
        tweets.forEach(function(tweet){
          appendTweet(tweet);
        });
      }
      else {
        appendErrMsgToHTML("一致するツイートがありません");
      }
    })
  });
});

かなり複雑なコードを追加しているように見えますが、全くそんなことはない。

先ほど削除した投稿情報のhtmlをもう一度作成しているだけである。

注意しなければいけない点は、index.html.erbで<%= %>で出力しているものをjbuilderで取得した値に変えていること。

<%= %>で出力しているものは、jbulider取得した値を${}で出力することができる。

次に、tweetsに投稿の情報が入っていない場合のappendErrMsgToHTML関数を定義する。

app/assets/javascripts/search.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
$(function() {

  var search_list = $(".contents.row");

  function appendTweet(tweet) {
    if(tweet.user_sign_in && tweet.user_sign_in.id == tweet.user_id){
      var current_user = `<li>
                            <a href="/tweets/${tweet.id}/edit" data-method="get" >編集</a>
                          </li>
                          <li>
                            <a href="/tweets/${tweet.id}" data-method="delete" >削除</a>
                          </li>`
    } else {
      var current_user = ""
    }

    var html = `<div class="content_post" style="background-image: url(${tweet.image});">
                  <div class="more">
                    <span><img src="/assets/arrow_top.png"></span>
                    <ul class="more_list">
                      <li>
                        <a href="/tweets/${tweet.id}" data-method="get" >詳細</a>
                      </li>
                      ${current_user}
                    </ul>
                  </div>
                  <p>${tweet.text}</p><br>
                  <span class="name">
                    <a href="/users/${tweet.user_id}">
                      <span>投稿者</span>${tweet.nickname}
                    </a>
                  </span>
                </div>`
    search_list.append(html);
   }

  function appendErrMsgToHTML(msg) {
    var html = `<div class='name'>${ msg }</div>`
    search_list.append(html);
  }

  $(".search-input").on("keyup", function() {
    var input = $(".search-input").val();
    $.ajax({
      type: 'GET',
      url: '/tweets/search',
      data: { keyword: input },
      dataType: 'json'
    })
    .done(function(tweets) {
      search_list.empty();
      if (tweets.length !== 0) {
        tweets.forEach(function(tweet){
          appendTweet(tweet);
        });
      }
      else {
        appendErrMsgToHTML("一致するツイートがありません");
      }
    })
  });
});

これもtweetsに値が入っている場合とやっていることが同じ。
コントローラーで検索をかけ、その投稿情報がなかった場合は、「一致するツイートがありません」という文字列を引数に渡してHTML要素を作成しビューに追加している。

 実際にインクリメンタルサーチができるか確認

下図のように、インクリメンタルサーチができれば成功。
https://tech-master.s3.amazonaws.com/uploads/curriculums//68835811e50d3c062c47b7b7a338a6ca.gif

6. エラー時の処理を行う

アラートで「投稿検索に失敗しました」と表示ができれば十分。

app/assets/javascripts/search.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
$(function() {

  var search_list = $(".contents.row");

  function appendTweet(tweet) {
    if(tweet.user_sign_in && tweet.user_sign_in.id == tweet.user_id){
      var current_user = `<li>
                            <a href="/tweets/${tweet.id}/edit" data-method="get" >編集</a>
                          </li>
                          <li>
                            <a href="/tweets/${tweet.id}" data-method="delete" >削除</a>
                          </li>`
    } else {
      var current_user = ""
    }

    var html = `<div class="content_post" style="background-image: url(${tweet.image});">
                  <div class="more">
                    <span><img src="/assets/arrow_top.png"></span>
                    <ul class="more_list">
                      <li>
                        <a href="/tweets/${tweet.id}" data-method="get" >詳細</a>
                      </li>
                      ${current_user}
                    </ul>
                  </div>
                  <p>${tweet.text}</p><br>
                  <span class="name">
                    <a href="/users/${tweet.user_id}">
                      <span>投稿者</span>${tweet.nickname}
                    </a>
                  </span>
                </div>`
    search_list.append(html);
  }

  function appendErrMsgToHTML(msg) {
    var html = `<div class='name'>${ msg }</div>`
    search_list.append(html);
  }

  $(".search-input").on("keyup", function() {
    var input = $(".search-input").val();
    $.ajax({
      type: 'GET',
      url: '/tweets/search',
      data: { keyword: input },
      dataType: 'json'
    })
    .done(function(tweets) {
      search_list.empty();
      if (tweets.length !== 0) {
        tweets.forEach(function(tweet){
          appendTweet(tweet);
        });
      }
      else {
        appendErrMsgToHTML("一致するツイートがありません");
      }
    })
    .fail(function() {
      alert('error');
    });
  });
});

サーバーエラーの場合、このfailの関数が呼ばれる。

以上でインクリメンタルサーチの実装は終了!

namespaceを利用したPicTweetにも実装する

7つのアクションのみで検索機能を実装した場合、つまり、namespaceを利用したルーティングやコントローラーの定義があるPictweetでも、インクリメンタルサーチを実装する方法を考える。

2つのPictweetの相違点

主に編集するファイル場所の違いと、それによるリクエストパスの違いになる。

  1. 検索に使用するコントローラーとビューの「名前」「場所」が異なる
  2. 構造が異なるため、ルーティングも異なる
  3. ルーティングが異なるため、検索のリクエストパスが異なる

コントローラーとビューの「名前」「場所」が異なる

7つのアクションのみで検索機能を実装した場合、searches_controller.rbの作成にnamespaceを利用。

そのため、コントローラーが格納されているディレクトリの構造が変わる。
app/controllers/tweets/searches_controller.rb

加えて、作成したsearches_controller.rbのindexアクションに対応するビューは、
app/views/tweets/searches/index.html.erbとして格納される。

これらの「namespaceを利用したことによる構造と名前の違い」がある。

ルーティングが異なる

namespaceを利用したことで、構造が変わったコントローラーになるため、
ルーティングも変更される。

【例】検索機能のルーティング
1
2
3
4
5
6
7
8
9
# searchアクション追加で実装した場合
$ rails routes
Prefix         Verb  URI Pattern               Controller#Action
search_tweets  GET   /tweets/search(.:format)  tweets#search

# namespaceを利用して7つのアクションのみで実装した場合
$ rails routes
Prefix           Verb  URI Pattern                 Controller#Action
tweets_searches  GET   /tweets/searches(.:format)  tweets/searches#index

検索のリクエストパスが異なる

ルーティングが変更されるため、それに伴って、検索機能を呼び出すリクエストを送信する処理を記述するJSファイルで、指定するリクエストパスを変更する必要がある。これらの違いを踏まえて、
インクリメンタルサーチを実装する。

0. 事前に必要な記述を確認

jQueryRailsに導入できているか確認する

【例】Gemfile
1
2
3
4
5
6
7
# 省略 以下末尾
gem 'pry-rails'
gem 'compass-rails', '3.1.0'
gem 'sprockets', '3.7.2'
gem 'devise'
gem 'kaminari'
gem 'jquery-rails'

上記記述が漏れていれば、追記してbundle installも実行。

【例】app/assets/javascripts/application.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's
// vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file. JavaScript code in this file should be added after the last require_* statement.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery
//= require rails-ujs
//= require activestorage
//= require turbolinks
//= require_tree .

textカラムにインデックスを設定

【例】ターミナル
1
$ rails g migration AddIndexToTweets
【例】db/migrate/20XXXXXXXXXXXX_add_index_to_tweets.rb
1
2
3
4
5
class AddIndexToTweets < ActiveRecord::Migration
  def change
    add_index :tweets, :text, length: 32
  end
end
【例】ターミナル
1
$ rails db:migrate

1. ルーティングなどAPI側の準備

アクションの中でHTMLとJSONなどのフォーマット毎に条件分岐するため、respond_toを使用する。

respond_toを利用してフォーマット毎に処理を分ける

記述する内容は全く同じだが、
今回編集するファイルはsearches_controller.rbのindexアクション。

【例】app/controllers/tweets/searches_controller.rb
1
2
3
4
5
6
7
8
9
class Tweets::SearchesController < ApplicationController
  def index
    @tweets = Tweet.search(params[:keyword])
    respond_to do |format|
      format.html
      format.json
    end
  end
end

「index.json.jbuilder」というファイルを以下のディレクトリに作成する

作成する場所となるディレクトリが異なる点に注意。
また、ファイル名は、index.json.jbuilderになる。

記述内容は同様。

【例】app/views/tweets/searches/index.json.jbuilder
1
2
3
4
5
6
7
8
json.array! @tweets do |tweet|
  json.id tweet.id
  json.text tweet.text
  json.image tweet.image
  json.user_id tweet.user_id
  json.nickname tweet.user.nickname
  json.user_sign_in current_user
end

2. テキストフィールドを作成

こちらは同様にすでに検索窓は作成されているとして、説明は割愛。

3. テキストフィールドが入力されるたびにイベントが発火するようにする

JSファイルを作成しますが、こちらは場所もファイル名も全て同じものになる。

search.jsというファイルを以下のディレクトリに作成

  •  app
    •  assets
      •  javascripts
        •  search.js

 

記述内容は、ここの段階ではまだ変わらない。

【例】app/assets/javascripts/search.js
1
2
3
4
5
$(function() {
  $(".search-input").on("keyup", function() {
    var input = $(".search-input").val();
  });
});

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

ここでAjaxを用いてリクエストを送信するが、ルーティングが異なるため、送信するリクエストパスのみ変更がある。

/tweets/searchとなっていたリクエストパスは、
/tweets/searchesへ変更している。

【例】app/assets/javascripts/search.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$(function() {
  $(".search-input").on("keyup", function() {
    var input = $(".search-input").val();
    $.ajax({
      type: 'GET',
      url: '/tweets/searches',
      data: { keyword: input },
      dataType: 'json'
    })
  });
});

5. 非同期通信の結果を得て、HTMLを作成

非同期通信の結果を元にビューを生成する。

同様の処理を記述していく。

【例】app/assets/javascripts/search.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
$(function() {
  $(".search-input").on("keyup", function() {
    var input = $(".search-input").val();
    $.ajax({
      type: 'GET',
      url: '/tweets/searches',
      data: { keyword: input },
      dataType: 'json'
    })
    .done(function(tweets) {
      $(".contents.row").empty();
      if (tweets.length !== 0) {
        tweets.forEach(function(tweet){
          appendTweet(tweet);
        });
      } else {
        appendErrMsgToHTML("一致するツイートがありません");
      }
    })
  });
});

 

同様に、tweetsに投稿の情報が入っている場合のappendTweet関数と、
tweetsに投稿の情報が入っていない場合のappendErrMsgToHTML関数を定義する。
記述内容は同じ。

【例】app/assets/javascripts/search.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
$(function() {

  var search_list = $(".contents.row");

  function appendTweet(tweet) {
    if(tweet.user_sign_in && tweet.user_sign_in.id == tweet.user_id){
      var current_user = `<li>
                            <a href="/tweets/${tweet.id}/edit" data-method="get" >編集</a>
                          </li>
                          <li>
                            <a href="/tweets/${tweet.id}" data-method="delete" >削除</a>
                          </li>`
    } else {
      var current_user = ""
    }

    var html = `<div class="content_post" style="background-image: url(${tweet.image});">
                  <div class="more">
                    <span><img src="/assets/arrow_top.png"></span>
                    <ul class="more_list">
                      <li>
                        <a href="/tweets/${tweet.id}" data-method="get" >詳細</a>
                      </li>
                      ${current_user}
                    </ul>
                  </div>
                  <p>${tweet.text}</p><br>
                  <span class="name">
                    <a href="/users/${tweet.user_id}">
                      <span>投稿者</span>${tweet.nickname}
                    </a>
                  </span>
                </div>`
    search_list.append(html);
  }

  function appendErrMsgToHTML(msg) {
    var html = `<div class='name'>${ msg }</div>`
    search_list.append(html);
  }

  $(".search-input").on("keyup", function() {
    var input = $(".search-input").val();
    $.ajax({
      type: 'GET',
      url: '/tweets/searches',
      data: { keyword: input },
      dataType: 'json'
    })
    .done(function(tweets) {
      search_list.empty();
      if (tweets.length !== 0) {
        tweets.forEach(function(tweet){
          appendTweet(tweet);
        });
      } else {
        appendErrMsgToHTML("一致するツイートがありません");
      }
    })
  });
});

6. エラー時の処理を行う

通信に失敗した場合の処理を実装して完了。

【例】app/assets/javascripts/search.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
$(function() {

  var search_list = $(".contents.row");

  function appendTweet(tweet) {
    if(tweet.user_sign_in && tweet.user_sign_in.id == tweet.user_id){
      var current_user = `<li>
                            <a href="/tweets/${tweet.id}/edit" data-method="get" >編集</a>
                          </li>
                          <li>
                            <a href="/tweets/${tweet.id}" data-method="delete" >削除</a>
                          </li>`
    } else {
      var current_user = ""
    }

    var html = `<div class="content_post" style="background-image: url(${tweet.image});">
                  <div class="more">
                    <span><img src="/assets/arrow_top.png"></span>
                    <ul class="more_list">
                      <li>
                        <a href="/tweets/${tweet.id}" data-method="get" >詳細</a>
                      </li>
                      ${current_user}
                    </ul>
                  </div>
                  <p>${tweet.text}</p><br>
                  <span class="name">
                    <a href="/users/${tweet.user_id}">
                      <span>投稿者</span>${tweet.nickname}
                    </a>
                  </span>
                </div>`
    search_list.append(html);
   }

  function appendErrMsgToHTML(msg) {
    var html = `<div class='name'>${ msg }</div>`
    search_list.append(html);
  }

  $(".search-input").on("keyup", function() {
    var input = $(".search-input").val();
    $.ajax({
      type: 'GET',
      url: '/tweets/search',
      data: { keyword: input },
      dataType: 'json'
    })
    .done(function(tweets) {
      search_list.empty();
      if (tweets.length !== 0) {
        tweets.forEach(function(tweet){
          appendTweet(tweet);
        });
      } else {
        appendErrMsgToHTML("一致するツイートがありません");
      }
    })
    .fail(function() {
      alert('error');
    });
  });
});