【Rails】検索機能、プルダウン選択によるソート機能

# はじめに
　勉強の一環としてフリーマーケットサイトのコピーを作成しています。
　その際に導入した<b>検索機能</b>と<b>ソート機能</b>について上手く実装できたので紹介します。
　コードが冗長となっている箇所や置き換えることのできるメソッドなど多くあるかと思いますので、その時はご指摘やご指導いただけると幸いです。

　※Rails5.0を使用しているため、　form_tagを使用しています。

# 導入したい機能と挙動
- 検索機能
- 出品日時や価格によるソート機能
- プルダウンを選択後にページ遷移する機能
- ソート後のプルダウンの固定

（ ※以下はダミーデータです。商品ではありません。）

![画面収録 2019-06-14 17.11.17.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418341/b9ebb3f6-61a7-38b2-911d-d68913ef66fc.gif)

# 使用ファイル
- header.html.haml
- search.html.haml
- products_controller.rb
- products_search.js

## 検索機能について

### 検索フォームビュー

```_header.html.haml
= form_tag('/search', method: :get, class: 'search-form') do
  %input.search-form__input{name: "keyword", placeholder: "何をお探しですか？", type: "text", value: ""}
  %button.btn-search{title: "検索", type: "submit"}
    = fa_icon "search",class: "search-form__icon"
```


　submitされた時に送られる値であるname属性を`keyword`とすることで、コントローラで params[:keyword]により値を受け取れます。
（ここではproductsコントローラー）

### 検索結果とプルダウン選択ビュー

```search.html.haml
.l-side
    .search-sort
      .select-wrap
        %select{name: :sort_order, class: 'sort_order'}
          %option{value: "location.pathname", name: "location.pathname"}
            並び替え
          %option{value: "price_asc"}
            価格の安い順
          %option{value: "price_desc"}
            価格の高い順
          %option{value: "created_at_asc"}
            出品の古い順
          %option{value: "created_at_desc"}
            出品の新しい順
```
　プルダウン選択後の値をjQueryで取得できるように、optionタグのvalue属性にそれぞれ値を記述します。
　商品検索結果の記述は省略します。

## 出品日時や価格によるソート機能

### productsコントローラー

```products_controller.rb
def search
  @keyword = params[:keyword]
  # orderメソッドへ代入する値を条件分岐
  # params[:sort].nil? ? sort  = "created_at DESC" : sort = params[:sort]をリファクタリング
  sort = params[:sort] || "created_at DESC"
  # 入力された値をLIKE句により各カラムと一致したものを抽出する。
  @products = Product.where('name LIKE(?) OR description LIKE(?)', "%#{@keyword}%", "%#{@keyword}%").order(sort)
  @count = @products.count
  # 検索結果が"0"だった場合、全ての商品を表示させる
  if @count == 0
    @products = Product.order(sort)
  end
end
```
　products変数末尾のorderメソッドには、選択したオプションタグのvalue属性を参照して送られてきた値によってソートするように、if文で条件分岐します。params[:sort]の初期値は`nil`となるため、`nil`には投稿新着順で表記するために`createcd_at DESC`を代入します。

### jQuery
```products_search.js
$(function() {
  // プルダウンメニューを選択することでイベントが発生
  $('select[name=sort_order]').change(function() {

    // 選択したoptionタグのvalue属性を取得する
    var this_value = $(this).val();
    // value属性の値により、ページ遷移先の分岐
    if (this_value == "price_asc") {
      html = "&sort=price+asc"
    } else if (this_value == "price_desc") {
      html = "&sort=price+desc"
    } else if (this_value == "created_at_asc") {
      html = "&sort=created_at+asc"
    } else if (this_value == "created_at_desc") {
      html = "&sort=created_at+desc"
    } else {
      html = ""
    };
    // 現在の表示ページ
    var current_html = window.location.href;
    // ソート機能の重複防止 
    if (location['href'].match(/&sort=*.+/) != null) {
      var remove = location['href'].match(/&sort=*.+/)[0]
      var current_html = current_html.replace(remove, '')
    };
    // ページ遷移
    window.location.href = current_html + html
  });
  // ページ遷移後の挙動
  $(function () {
    if (location['href'].match(/&sort=*.+/) != null) {
      // option[selected: 'selected']を削除
      if ($('select option[selected=selected]')) {
        $('select option:first').prop('selected', false);
      }

      var selected_option = location['href'].match(/&sort=*.+/)[0].replace('&sort=', '');

      if(selected_option == "price+asc") {
        var sort = 1
      } else if (selected_option == "price+desc") {
        var sort = 2
      } else if (selected_option == "created_at+asc") {
        var sort = 3
      } else if (selected_option == "created_at+desc") {
        var sort = 4
      }

      var add_selected = $('select[name=sort_order]').children()[sort]
      $(add_selected).attr('selected', true)
    }
  });
});

```

#### URLについて

　URLに`&sort=price+asc`と記述することで、コントローラーの`params[:sort]`へ`price asc`が送られ、該当テーブルの`priceカラム`を昇順(ASC, ascending)に表示します。

```url
http://localhost:3000/search?utf8=✔︎&keyword=ねこ&sort=price+asc
```

　したがって、optionタグのvalue属性の値により、該当する値を取得します。

```products_search.js
if (this_value == "price_asc") {
  html = "&sort=price+asc"
} else if (this_value == "price_desc") {
  html = "&sort=price+desc"
} else if (this_value == "created_at_asc") {
  html = "&sort=created_at+asc"
} else if (this_value == "created_at_desc") {
  html = "&sort=created_at+desc"
} else { 
  html = ""
};
```

　window.location.hrefメソッドによりページを遷移することができるため、if文の戻り値を現在のURLへ加えることで表示している商品データを、希望の条件にソートすることができました。

```js
window.location.href = window.location.href + html
```

　2度以上オプションタグを選択すると、以下のように重複して記述してしまいます。

```url
URL省略）~keyword=ねこ&sort=price+asc&sort=price+desc&sort=price+asc
```

　`location['href']`により現在のURLを取得して、正規表現により最初に該当した`&sort=`とそれ以降の文字列を取得し、replaceメソッドにより<b>無に返しました。</b>
　また、最初期のURLには該当文字列がないため、エラー回避をif文で行います。


```products_search.js
if (location['href'].match(/&sort=*.+/) != null) {
  var remove = location['href'].match(/&sort=*.+/)[0]
  var current_html = current_html.replace(remove, '')
};
```


#### ここまでの記述でプルダウンによるソートは完了しました。
*しかし、ソートするたびにページを更新するため、optionタグが初期値である`並び替え`に戻ってしまいます。*

![画面収録 2019-06-14 19.34.00.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418341/afaf0fe8-7bbb-80b9-de4b-23ff1b2f9124.gif)

そのため、ページ遷移後にoptionタグへ
#### selected属性を付与させることで選択肢をあらかじめ選択済みにさせます</b>。

## ソート後のプルダウンの固定

　 `$(function ()`により、ページが読み込み完了してDOMの構築が完了した時点でfunction()内が実行されます。現在の表示ページURLに`&sort=`があった場合のみイベントを実行させます。

```products_search.js
 // ページ遷移後の挙動
  $(function () {
    // 表示URLに"&sort="がある場合のみ実行
    if (location['href'].match(/&sort=*.+/) != null) {
      // 現在のoptionタグにselected属性が与えられている場合、削除する。
      if ($('select option[selected=selected]')) {
        $('select option:first').prop('selected', false);
      }
      // 表示ページURLの"/&sort="以下記述を取得して、replaceメソッドにより"/&sort="を無に返します。
      var selected_option = location['href'].match(/&sort=*.+/)[0].replace('&sort=', '');
      // 上記で取得した値により条件分岐する。
      if(selected_option == "price+asc") {
        var sort = 1
      } else if (selected_option == "price+desc") {
        var sort = 2
      } else if (selected_option == "created_at+asc") {
        var sort = 3
      } else if (selected_option == "created_at+desc") {
        var sort = 4
      }
      // selectタグ内のoptionタグをchildreメソッドで取得すると配列で返されるため、
      // 上記でif文で取得した値を、代入すると、選択したオプションタグを取得することができる。
      var add_selected = $('select[name=sort_order]').children()[sort]
      // 選択したオプションタグへpropメソッドにより、seletcted属性を付与する。
      $(add_selected).prop('selected', true)
    }
  });
```

### 上の記述により、選択したプルダウンを固定することができました！！

# あとがき
　この機能の実装を考えている間はとても楽しく過ごすことができました。
　エンジニアの皆様からすると、記述が冗長な箇所やそもそも活用されていない記述が見受けられるかもしれません。その際は、ご指摘やご指導いただけますと幸いでございます。よろしくお願いします。

## 参考文献
- https://magai.hateblo.jp/entry/2018/03/06/144732