20190622のRailsに関する記事は15件です。

2019年にRails + Ajaxを整理してみる(サンプルアプリ&コード付き)

Rails上でAjaxを動かす、という良くありそうな話。

ただ調べてみるとやり方が色々あって、
Rails歴半年ちょいの私には何が正しいのかさっぱり分からなかった。

という訳で整理してみる。同じ境遇にある人の助けになれば嬉しい。

jQueryとかを用いた、古式ゆかしい(らしい)やり方ですので、
Vue/React等をお使いの方々はおかえりください(涙)

間違ってる部分とかあったら、コメントいただければ幸いです。
(特に図の部分)

この記事がよく刺さりそうな人

  • Railsの基礎はわかる
  • Ajaxの雰囲気はわかる
  • JavaScript & jQueryも本気出せばちょっと書ける
    (決してチョットデキルではない)
  • RailsでAjaxはあまりやった事がない
    もしくは「良く分からんけどまぁ動いてるからヨシ!」で乗り切った

とりあえず結論

Rails + Ajax の実現方法は、
ざっくり以下の3パターン&その組み合わせっぽい。

1. Rails推奨方式
2. フロントはJavaScriptだけでやる方式
3. AjaxのリクエストはRails & レスポンス以降はJavaScriptでやる方式

方式別サンプルアプリ&コード

作ったのは、フォームに文字を入力&ボタンを押すと、Ajaxを使って文字が書き換わるアプリ。
(アプリを名乗るのはおこがましいかもしれない)

これを上の3方式のそれぞれでやってみた。
Ajaxでやる意味ある?というツッコミは無しで...

ajax-succeed.gif

コードは本当に必要最低限なので、色々細かいツッコミはご容赦くださいm(_ _)m
逆に手元で再現する分にはやりやすいはず。。。

あとjQueryを使ってます。入れ方は以下参照。
Rails 5.2 jQuery 動かし方 - Qiita

共通処理

サーバ(Rails)側の処理は、3つの方式でほぼ共通。

routes.rb
Rails.application.routes.draw do
  get 'static/top'
  post 'static/ajax_update', to: 'static#ajax_update'
  post 'static/ajax_update2', to: 'static#ajax_update2'
end
static_controller.rb
class StaticController < ApplicationController
  def top
  end

  # 1. Rails推奨方式 で使用
  def ajax_update
    @text = params[:data]
    render
  end

  # 2. フロント側はJavaScriptだけでやる方式 
  # 3. AjaxのリクエストはRails & レスポンス以降はJavaScriptでやる方式
  # で使用
  def ajax_update2
    @text = params[:data]
    render plain: @text
  end
end

1. Rails推奨方式

Railsガイドに書かれたやり方
Rails で JavaScript を使用する - Rails ガイド

ajax_update.js.erb
var user = '<%= "#{ @text }" %>'
$('#ajax-test1').text(user);
top.html.slim
h1 Static#top
p Find me in app/views/static/top.html.slim

#ajax-test1 Ajax: Rails依存

#ajax-request1
  = form_with url: static_ajax_update_path do |f|
    = f.text_field :data
    = f.submit 'Post Ajax'

図に表すと多分以下の感じ。
もうガッツリRailsに乗っかっている状態。

rails-ajax.001.jpeg

肝は以下2点

  • xxx.js.erbからJavaScriptをレンダリングしてフロントに返す
  • フロント側で受け取ったJavaScriptを実行

個人的にはRails側で、JavaScriptをレンダリングしている辺り、
少し気持ち悪い。。。

ただコード量は必要最低限で済むし、
Rails推奨であることからトラブルも起きにくそう。
基本はこれでいいのではないだろうか。

なお細かい処理がしたい場合には不便になることもある様子で、
何だかんだ使わないと言う話もあるらしい。
参考:https://qiita.com/ka215/items/dfa602f1ccc652cf2888

2. フロント側はJavaScriptだけでやる方式

Railsにあえて叛逆していくやり方。

top.html.slim
#ajax-test4 Ajax: ほぼJS(jQuery)
= text_field_tag 'static[ajax_data2]'
= button_tag 'Post Ajax', id: 'btn2'
ajax_request_response.js
$(document).ready( () => {
  $('#btn2').on('click', (e) => {
    e.preventDefault();

    const param = $('#static_ajax_data2').val();

    // CSRFトークンを取得&セット
    $.ajaxPrefilter( (options, originalOptions, jqXHR) => {
        if (!options.crossDomain) {
          const token = $('meta[name="csrf-token"]').attr('content');
          if (token) {
               return jqXHR.setRequestHeader('X-CSRF-Token', token);
           }
        }
    });

   $.ajax({
      url: `/static/ajax_update2`,
      type: 'POST',
      data: {
        data: param
      }
    })
    .done( (data, textStatus, jqXHR) => {
      var result = $('#ajax-test4');
      result.text(data);
    });
  });
});

図に表すと多分以下の感じ。
Ajaxに関しては、Railsには頼らないという強い意思が見える。

rails-ajax.002.jpeg

肝は、
RailsのCSRF対策のために、
CSRFトークンの取得&セットを行なっている所。

具体的なやり方は以下の記事を完全リスペクトしましたm(_ _)m
https://qiita.com/a_ishidaaa/items/7c3fa339d3bea25a9ba8

ざっくり言うと、RailsではCSRFという脆弱性への対策として、
Postのリクエスト時にトークン(身分証明みたいなもの)を使っている。
ここをカバーしてあげないと、JavaScriptからPostは出来ない。

そう、Railsからの叛逆に成功したと思いきや、
実はその呪縛から逃れきれていなかったのだ。
なんかエモい。

なお、RailsのCSRFについての詳細は以下の記事等をご参照ください。
外部からPOSTできない?RailsのCSRF対策をまとめてみた - Qiita

3. AjaxのリクエストはRails & レスポンス以降はJavaScriptでやる方式

Railsへの依存を減らしつつ、CSRF対策はRailsによろしくできるやり方。

top.html.slim
= form_with url: static_ajax_update2_path, id: 'ajax-request-3' do |f|
  = f.text_field :data
  = f.submit 'Post Ajax'
ajax_response.js
// Ajax: form送信はRails、受信以降はJS
$( () => {
  $('#ajax-request-3').on('ajax:success', (e) => {
    const result = $('#ajax-test3');
    result.text(e.detail[0]);
  });
});

図に表すと多分以下の感じ。
折衷案な雰囲気。

rails-ajax.003.jpeg

肝は、
RailsとJavaScriptの間で、
どのようにデータをやり取りされるかの理解が必要な所。

適当にやってると変なハマり方をしそう。。。

ただそこさえクリアすれば、
Railsっぽさと自由度をある程度両立できる?気がする(よく分かってない)

まとめ

  • 基本的には大人しく「1. Rails推奨方式」を使った方がいい気がする。
    (特に経験浅めの人)
  • ただ不便な場合もある(らしい)ので、
    その際は「3. AjaxのリクエストはRails...」を採用、
    もしくは「1. Rails推奨方式」と組み合わせて使えば良さそう。
  • Railsで開発するけどなるべく依存したくないというワガママな人は、
    「2. フロント側はJavaScriptだけでやる方式」を使えばいい...のか?

参考サイト

以下本記事作成に際しお世話になったサイト。見ると理解がすごく深まる。。。
Ruby on RailsのAjax処理のおさらい - Qiita
Rails 5.1+jQueryでajaxを試す (罠にハマる) - Qiita
Rails 5.2 jQuery 動かし方 - Qiita
RailsでのAjax - Qiita
jQuery.ajax()のまとめ: 小粋空間
Rails 5.2 jQuery 動かし方 - Qiita

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

railsで複数ワードでの検索機能(end)とマイナス検索機能(-)を実装してみる

はじめに

 半年前にrailsで複数ワードでの検索機能(or)とマイナス検索機能(-)を実装してみるという記事を投稿させてもらったのですが、沢山の方に見てもらい良質なフィードバックまで頂きました。本当にありがとうございます!
 そこで記事の続きという訳では無いですが、複数ワードでの検索時にorでは無くendで検索ができるようなコードをフィードバックを踏まえて書いてみました。また今回も完成品のコードを最後に置いておきます。

環境

Ruby 2.3.3
Rails 5.2.3
MySQL 8.0.13

前提として

まず前提としてこんな感じの検索フォームからコントローラの方に検索ワードを送る。

index.html.erb
<%= form_tag('/items/search', method: :get) do %>
   <input id="page_name" name='keyword' size="30" type="text" />
<% end %>

その後にコントローラで入力された検索ワードに合わせてデータを引っ張ってきて表示する。

search.html.erb
<% @items.each do |item| %>
  <%= item.name %>
<% end %>

ちなみにDBにはこんなデータが入っているとします。

mysql> select name from items;
+------------------+
| name             |
+------------------+
| 水筒A            |
| 水筒B            |
| 水筒C            |
| 大きい水筒       |
| 大きい水筒A      |
| 小さい水筒       |
| 小さい水筒A      |
+------------------+
7 rows in set (0.00 sec)

前提のコード(orとマイナス検索)

# キーワード分割
keywords = params[:keyword].split(/[[:blank:]]+/).select(&:present?)

# 普通のキーワードとマイナスのキーワードを分ける
negative_keywords, positive_keywords = 
  keywords.partition {|keyword| keyword.start_with?("-") }

# 空のモデルオブジェクト作成(何も入っていない空配列のようなもの)
@items = Item.none

# 検索ワードの数だけor検索を行う
positive_keywords.each do |keyword|
  @items = @items.or(Item.where("name LIKE ?", "%#{keyword}%"))
end

# -(マイナス)がついた検索ワードの数だけnot検索を行う
negative_keywords.each do |keyword|
  @items.where!("name NOT LIKE ?", "%#{keyword.delete_prefix('-')}%")
end

前回の記事でkg8mさんから教えて頂いた複数ワードでの検索機能(or)とマイナス検索機能(-)のコードです。短くて素敵。これをorからandに変えていく形で解説を入れながらコードを書いていきます。

実際に書いてみる

必要な作業は
1,送られてきたキーワードを空白で区切る(キーワード分割)
2,普通のキーワードと-(マイナス)のついたキーワードを分ける
3,普通のキーワード群でAND検索を行う
4,-(マイナス)のキーワードでNOT検索を行う

早速1から行きます。

1、送られてきたキーワードを空白で区切る(キーワード分割)

検索フォームから送られてきた「水筒 A -小さい」のようなキーワードの文字列を「"水筒","A","-小さい"」と空白で区切って複数のワードに分ける作業です。この時はまだマイナスは気にしません。

items_controller.rb
def search
  # キーワード分割
  keywords = params[:keyword].split(/[[:blank:]]+/).select(&:present?)
end

params[:keyword]に検索フォームで入力された検索ワードが入っています。
それをsplitメソッドで分割を行います。[:blank:]は簡単に言ったら空白やタブという意味です(この記事を参考にしました)。つまり空白で区切って配列にするぜ!という意味になります。

その後にselectメソッド配列から何も入っていない要素を削除します。このメソッドの使い方はリファレンスを見るとわかりやすい。あ、present?はちょっと説明が難しいのですが何か値があるか?で真と偽を返すようです。
そもそも空白で区切ってなんで配列の要素にそんなのがあんだよ!ってなるんですが、前回の記事のここに理由を書いてあるので気になった方は読んでみてください。

2、普通のキーワードと-(マイナス)のついたキーワードを分ける

「"水筒","A","-小さい"」のように配列になったキーワード群を普通のキーワードの配列とマイナスのキーワードの配列に分けます。

items_controller.rb
def search
  # キーワード分割
  keywords = params[:keyword].split(/[[:blank:]]+/).select(&:present?)

  # 普通のキーワードとマイナスのキーワードを分ける
  negative_keywords, positive_keywords = 
    keywords.partition {|keyword| keyword.start_with?("-") }

end

keywords に配列で検索ワードが入っています。これをpartitionメソッドで普通の検索ワードが入った配列とマイナスの検索ワードが入った配列に分けます。
partitionメソッドは配列の要素1つ1つを調べて真になったら1つめの配列型の変数に(今回の場合はnegative_keywords)、偽だったら2つめの配列型の変数に(今回の場合はpositive_keywords)に入れ直してくれます。
今回は1つ1つのキーワードに対してstart_with?("-")としているので要素の先頭が「-」だったらnegative_keywordsに要素を入れるといった動きになります。

3,普通のキーワード群でAND検索を行う

ここが一番大切なポイントです!この部分はor検索の時は以下のようになっていました。

items_controller.rb
# 検索ワードの数だけor検索を行う
positive_keywords.each do |keyword|
  @items = @items.or(Item.where("name LIKE ?", "%#{keyword}%"))
end

キーワードを1つづつwhereで検索をかけてそれをorで繋げる感じ。ただ今回はandなので普通にwhere句を重ねていく。

items_controller.rb
def search
  # キーワード分割
  keywords = params[:keyword].split(/[[:blank:]]+/).select(&:present?)

  # 普通のキーワードとマイナスのキーワードを分ける
  negative_keywords, positive_keywords = 
    keywords.partition {|keyword| keyword.start_with?("-") }

  # Itemモデルオブジェクト作成
  @items = Item

  # 検索ワードの数だけand検索を行う
  positive_keywords.each do |keyword|
    @items = @items.where("name LIKE ?", "%#{keyword}%")
  end
end

これでいけるはず。
ただし@items = Itemはどうなんでしょうかね?これOKなんですかね?

4,-(マイナス)のキーワードでNOT検索を行う

ここは元の所と変える必要が無かった。はずだった。
とりあえず元のコードを解説。

items_controller.rb
# -(マイナス)がついた検索ワードの数だけnot検索を行う
negative_keywords.each do |keyword|
  @items.where!("name NOT LIKE ?", "%#{keyword.delete_prefix('-')}%")
end

NOT LIKEでキーワードに引っかかったデータを除外している。delete_prefixメソッドは文字列の先頭に引数の文字があれば削除するというもの。negative_keywordsにはマイナスキーワードが ["-小さい","-コンパクト"] といった感じで入っていますが、これをこのまま「-小さい」で検索しても「小さい」にはヒットしません。
なのでdelete_prefixを使い先頭の-を削除してから検索をしている訳です。

で、問題はここから。このdelete_prefixメソッドはRubyのバージョンが2.5で実装されたメソッドなので自分の開発環境の2.3では使えない。このメソッドを教えて頂いた前回の記事ではあろうことか開発環境の項目にrailsとMysqlだけでRubyのバージョンを書いていないというアホみたいな事をやらかしているという・・・すまねぇすまねぇ。

しょうがないのでdelete_prefixを使わない方向で実装する。

items_controller.rb
def search
  # キーワード分割
  keywords = params[:keyword].split(/[[:blank:]]+/).select(&:present?)

  # 普通のキーワードとマイナスのキーワードを分ける
  negative_keywords, positive_keywords = 
    keywords.partition {|keyword| keyword.start_with?("-") }

  # Itemモデルオブジェクト作成
  @items = Item

  # 検索ワードの数だけand検索を行う
  positive_keywords.each do |keyword|
    @items = @items.where("name LIKE ?", "%#{keyword}%")
  end

  # マイナスキーワードの先頭から-を取り除く
  negative_keywords.each {|word| word.slice!(/^-/) }

  # マイナスキーワードでnot検索
  negative_keywords.each do |keyword|
    next if keyword.blank?
    @items = @items.where.not("name LIKE ?", "%#{keyword}%")
  end
end

まずslice!メソッドでマイナスキーワードから先頭の-を取り除く。そして後はnot検索を行います。keyword.blank?は「""」みたいな要素が来た時の対策です。詳しくは前回の記事のここを見てみてください。

これで完成です!

改善点

@items = Itemがちょっと気になる。
元々のor検索では@items = Item.noneだったけど、and検索だと常に検索結果が0件になる。推測だけどnoneは空のモデルを取得するって意味らしいが、アクションレコードとしてSQLが発行されて何もヒットしなかった(だから空のモデル)扱いになってるのかな?
だとすると「"水筒","大きい"」で検索した場合条件は
1.none(ヒット無し)
2.水筒
3.大きい
になるから、orだったら1~3のどれかに合致すればデータを引っ張ってこれたけど、andだと全ての条件に合致しなければいけないから何のデータにも引っかからないnoneがあると検索結果が常に0件になるんだと思う。なので@items = Item.allで取り敢えず全データを入れてたけれど、all無しでも動いたので無しで動かしてる。アクションレコードに関しては勉強不足だなー。

完成品(コード)

items_controller.rb
  def search
    keywords = params[:keyword].split(/[[:blank:]]+/).select(&:present?)
    negative_keywords, positive_keywords =
    keywords.partition {|keyword| keyword.start_with?("-") }

    @items = Item

    positive_keywords.each do |keyword|
      @items = @items.where("name LIKE ?", "%#{keyword}%")
    end

    negative_keywords.each {|word| word.slice!(/^-/) }

    negative_keywords.each do |keyword|
      next if keyword.blank?
      @items = @items.where.not("name LIKE ?", "%#{keyword}%")
    end

  end

あと今回のコードは検索フォームにキーワードが入力されていなかった場合の処理を書いていないので必要に応じて付け足してください。

おわりに

Ruby(Rails)は面白いメソッドが多いと思う。自分でガリガリ書かなくても良いようになっていて便利だと感じています。あと久しぶりに記事を書いたからマトモに書けてるか心配です・・・

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

現場で使うコントローラーの作成方法

概要

プログラミングスクールなどで習うrails g controller コントローラー名というコマンドでのコントローラー作成方法は、実際の現場では使用する頻度が少ないという。では実際の現場で使うコントローラー作成方法をどのようなものなのか。簡単にまとめてみた。

なぜ現場ではrails g controller コントローラー名というコマンドで作成しないのか?

結論!!余計なファイルができると管理しづらい!

これに尽きるということ!

では実際はどうやるのか…

現場で使うコントローラー作成方法とは

1.app/controllers/の中に新しいファイルを作成。

app/controllers/ディレクトリの中にコントローラー名_controller.rbという新しいファイルを作成する。

2.コントローラー名_controller.rbに、コントローラークラスを作成していく。

class コントローラー名Controller 

end

これだけだと、クラスを定義しただけなので、コントローラーとして機能しない。

なので、app/controllers/application_controllerの機能を引き継いであげる。(< ApplicationControllerを追加)

class コントローラー名Controller < ApplicationController

end

これで、コントローラー名Controllerが、コントローラーとしての役割をしてくれるため、

結果 →  コントローラー作成!!(ApplicationControllerの機能をすべて引き継いだ)

まとめ

以上、現場で使うコントローラー作成方法をまとめてみた。今回は実際に現場で働いた実績のあるフリーランスエンジニアの方の方法を自分なりに噛み砕いて、アウトプット。初学者ないし、これからエンジニアとして現場に入る方の参考になれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

credentials.yml.encでRails環境変数の管理

前にdotenvを使ったRails環境変数の設定方法について記事を書いた
dotenvを使ったRails環境変数設定)ところ、
Rails5.2以降ならcredentials.yml.encで環境変数管理ができると教えて頂いたので、備忘録を兼ねて書きます。

参考記事

Rails5.2から追加された credentials.yml.enc のキホン

credentials.yml.encの編集

config配下にcredentials.yml.encというファイルがアプリを作成した時に作られている。ここに認証情報を定義する。

credentials.yml.encを編集するためにはrails credentials:editを実行する必要がある。(ここではEDITORが未指定なのでEDITOR="vi"としている)

ルートディレクトリで以下を実行

$ EDITOR="vi" bin/rails credentials:edit

認証情報を記載

hoge_keyを追加

credentials.yml.enc
# aws:
#   access_key_id: 123
#   secret_access_key: 345

 hoge:
    hoge_key:hogehogehoge

# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
secret_key_base: 43f416e170882b7f2d3422e9c8d74c81664d9728851531.......

認証情報を取り出す時

hoge_key = Rails.application.credentials[:hoge_key]

credentials.yml.encはmaster keyを利用して暗号化・復号され、master keyの値はconfig/master.keyに記載されている。
master.keyはデフォルトで.gitignoreに含まれており、master keyがGitリポジトリに含まれないようになっている。

.gitignoreを確認

.gitignore
# Ignore master key for decrypting credentials and more.
/config/master.key

これで認証情報を含んだソースをGitに上がるのを防ぐことができた。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】一覧ページ上部に検索機能を実装する ~ form_with ~

こんにちは!Railsエンジニア歴10ヶ月のshin1rokです!
プログラミング初心者の友人に検索機能の実装方法を伝授したので、その内容を公開します。

バージョン

  • Ruby 2.6.3
  • Rails 5.2.3
  • bootstrap 4.3.1(レイアウト調整のため。検索機能実装に必須ではない。)

完成イメージ

スクリーンショット 2019-06-02 14.30.53.png

コード

# app/views/users/index.html.erb
<p id="notice"><%= notice %></p>

<h1>Users</h1>

<div class="search-form">
  <%= form_with(scope: :search, url: users_path, method: :get, local: true) do |f| %>
    <div class="field">
      <%= f.label(:name, User.human_attribute_name(:name)) %>
      <%= f.text_field :name, value: @search_params[:name] %>
    </div>

    <div class="field">
      <%= f.label(:gender, User.human_attribute_name(:gender)) %>
      <%= f.collection_radio_buttons(:gender, User.genders, :first, :first) do |r| %>
        <%= tag.div(class: 'form-check form-check-inline') do %>
          <%= r.radio_button(checked: r.value == @search_params[:gender], class: 'form-check-input') + f.label(User.human_attribute_name("gender.#{r.text}"), class: 'form-check-label') %>
        <% end %>
      <% end %>
    </div>

    <div class="field">
      <%= f.label(:birthday, User.human_attribute_name(:birthday)) %>
      <%= f.date_field :birthday_from, value: @search_params[:birthday_from] %> ~ <%= f.date_field :birthday_to, value: @search_params[:birthday_to] %>
    </div>

    <div class="field">
      <%= f.label(:prefecture_id, User.human_attribute_name(:prefecture_id)) %>
      <%= f.collection_select(:prefecture_id, Prefecture.all, :id, :name, selected: @search_params[:prefecture_id], include_blank: t('helpers.select.include_blank')) %>
    </div>

    <div class="actions">
      <%= f.submit(t('helpers.submit.search')) %>
    </div>
  <% end %>
</div>

<table>
  <thead>
  <tr>
    <th>Name</th>
    <th>Gender</th>
    <th>Birthday</th>
    <th>Prefecture</th>
    <th colspan="3"></th>
  </tr>
  </thead>

  <tbody>
  <% @users.each do |user| %>
    <tr>
      <td><%= user.name %></td>
      <td><%= user.gender %></td>
      <td><%= user.birthday %></td>
      <td><%= user.prefecture.name %></td>
      <td><%= link_to 'Show', user %></td>
      <td><%= link_to 'Edit', edit_user_path(user) %></td>
      <td><%= link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?' } %></td>
    </tr>
  <% end %>
  </tbody>
</table>

<br>

<%= link_to 'New User', new_user_path %>
app/controllers/users_controller.rb
class UsersController < ApplicationController

  # indexアクション以外は不要なので省略しています。
  def index
    @search_params = user_search_params
    @users = User.search(@search_params).includes(:prefecture)
  end

  private

  def user_search_params
    params.fetch(:search, {}).permit(:name, :gender, :birthday_from, :birthday_to, :prefecture_id)
  end
end
app/models/user.rb
class User < ApplicationRecord
  enum gender: { unanswered: 0, male: 1, female: 2, other: 9 }

  belongs_to :prefecture

  scope :search, -> (search_params) do
    return if search_params.blank?

    name_like(search_params[:name])
      .gender_is(search_params[:gender])
      .birthday_from(search_params[:birthday_from])
      .birthday_to(search_params[:birthday_to])
      .prefecture_id_is(search_params[:prefecture_id])
  end
  scope :name_like, -> (name) { where('name LIKE ?', "%#{name}%") if name.present? }
  scope :gender_is, -> (gender) { where(gender: gender) if gender.present? }
  scope :birthday_from, -> (from) { where('? <= birthday', from) if from.present? }
  scope :birthday_to, -> (to) { where('birthday <= ?', to) if to.present? }
  scope :prefecture_id_is, -> (prefecture_id) { where(prefecture_id: prefecture_id) if prefecture_id.present? }
end

GitHub shin1rok/rails_search_sample

事前準備

デバッグを効率よく行うためにBetter Errorsをインストールします。
使い方は公式をみていただければと思うのですが、インストール手順を記載しておきます。

1. gemfileに以下を追記する

group :development do
  gem 'better_errors'
  gem 'binding_of_caller'
end

2. bundle install

3. 適当なところでraiseしてインストールできていることを確認する

※プログラミング全般に言えることですが、確認を怠ると不具合が発生した際の原因の切り分けが難しくなります。めんどうに感じるかもしれませんが、1つ1つ確認しながら進めた方が結果として近道になります。

実装方針

  • 検索パラメータを入力するフォームを作る(View)
  • 検索パラメータを受け取る(Controller)
  • 検索処理を実行する(Model)

各検索項目について

  • 名前: like検索
  • 性別: ラジオボタンで選択
  • 誕生日: 範囲検索
  • 都道府県: プルダウン選択

テーブル構成

ユーザテーブルと都道府県テーブルを作成します。
ユーザテーブルにprefecture_idを外部キーとして設定するため、都道府県テーブルから先に作成する必要があります。

db/schema.rb
ActiveRecord::Schema.define(version: 2019_05_27_215135) do

  create_table "prefectures", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
    t.string "name"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "users", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
    t.string "name"
    t.integer "gender", default: 0, null: false
    t.date "birthday"
    t.bigint "prefecture_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["prefecture_id"], name: "index_users_on_prefecture_id"
  end

  add_foreign_key "users", "prefectures"
end

コマンド例

rails g model prefecture name:string --skip-test-unit --no-api --no-jbuilder
rails g scaffold user name:string gender:integer birthday:date prefecture:references --skip-test-unit --no-api --no-jbuilder

オプションの詳細は「rails scaffold option」とかで調べてみてください。

View

Viewで行なっていることは以下の通りです。順番に解説していきます。

  • form_withで入力フォームを作る
    • 検索後に検索フォームに入力された値を保持するようにする
  • I18n対応
  • ラジオボタンを横並びにする(Bootstrap4)

※ Viewの解説が一番難しい & 長いので、さらっと読み飛ばして、先にController, Modelを読む方がいいかもです。

再掲

# app/views/users/index.html.erb
<p id="notice"><%= notice %></p>

<h1>Users</h1>

<div class="search-form">
  <%= form_with(scope: :search, url: users_path, method: :get, local: true) do |f| %>
    <div class="field">
      <%= f.label(:name, User.human_attribute_name(:name)) %>
      <%= f.text_field :name, value: @search_params[:name] %>
    </div>

    <div class="field">
      <%= f.label(:gender, User.human_attribute_name(:gender)) %>
      <%= f.collection_radio_buttons(:gender, User.genders, :first, :first) do |r| %>
        <%= tag.div(class: 'form-check form-check-inline') do %>
          <%= r.radio_button(checked: r.value == @search_params[:gender], class: 'form-check-input') + f.label(User.human_attribute_name("gender.#{r.text}"), class: 'form-check-label') %>
        <% end %>
      <% end %>
    </div>

    <div class="field">
      <%= f.label(:birthday, User.human_attribute_name(:birthday)) %>
      <%= f.date_field :birthday_from, value: @search_params[:birthday_from] %> ~ <%= f.date_field :birthday_to, value: @search_params[:birthday_to] %>
    </div>

    <div class="field">
      <%= f.label(:prefecture_id, User.human_attribute_name(:prefecture_id)) %>
      <%= f.collection_select(:prefecture_id, Prefecture.all, :id, :name, selected: @search_params[:prefecture_id], include_blank: t('helpers.select.include_blank')) %>
    </div>

    <div class="actions">
      <%= f.submit(t('helpers.submit.search')) %>
    </div>
  <% end %>
</div>

<table>
  <thead>
  <tr>
    <th>Name</th>
    <th>Gender</th>
    <th>Birthday</th>
    <th>Prefecture</th>
    <th colspan="3"></th>
  </tr>
  </thead>

  <tbody>
  <% @users.each do |user| %>
    <tr>
      <td><%= user.name %></td>
      <td><%= user.gender %></td>
      <td><%= user.birthday %></td>
      <td><%= user.prefecture.name %></td>
      <td><%= link_to 'Show', user %></td>
      <td><%= link_to 'Edit', edit_user_path(user) %></td>
      <td><%= link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?' } %></td>
    </tr>
  <% end %>
  </tbody>
</table>

<br>

<%= link_to 'New User', new_user_path %>

form_withで入力フォームを作る

入力フォームを実装するためには大きく2ステップあります。

  1. 入力フィールドをinputタグで作る(text_field, collection_radio_buttons, collection_select, date_fieldなど)
  2. 入力された情報をformタグによってサーバに送れるようにする(form_with)

Railsではそれらを効率よく作るためにViewHelper(↑のかっこ内のメソッドのことです。)が用意されています。
ViewHelperを使いこなせるようになると一気に生産性が高まるので、可能な限りViewHelperを使っていきましょう。1
とりあえず動くようにする → ブラウザからDeveloperToolsでHTMLを確認 → 正しく修正、というのを繰り返すことで少しずつ身に付いていくと思います。

Railsガイド Action View フォームヘルパー

※Rails5.1以上の場合、form_for, form_tagではなくて、form_withを使いましょう。
Railsガイド form_forとform_tagのform_withへの統合

form_with

form_withメソッドを用いてformタグを生成できるようにします。

Ruby on Rails API - form_with

検索パラメータはControllerにおいてparams[:search]のHash形式(厳密にはHashではなくActionController::Parametersオブジェクトです。)で受け取りたいので、そのように実装します。

こちらがミニマムで作成した検索フォームです。
(formだけだと検索できないので、名前の入力欄もついでに作っています。)
スクリーンショット 2019-06-12 23.52.00.png

# erb
<%= form_with(scope: :search, url: users_path, method: :get, local: true) do |f| %>
  <%= f.label(:name, '名前') %>
  <%= f.text_field :name %>
  <%= f.submit('検索') %>
<% end %>
# ブラウザから見たHTML
<form action="/users" accept-charset="UTF-8" method="get"><input name="utf8" type="hidden" value="✓">
  <label for="search_name">名前</label>
  <input type="text" name="search[name]" id="search_name">
  <input type="submit" name="commit" value="検索" data-disable-with="検索">
</form>

スクリーンショット 2019-06-12 23.59.01.png

察しの良い方は上記のerbとHTMLを見比べたらわかるかもですが、説明していきます。

scope: :search

scope: :searchf.text_field :nameとすることでparams[:search]nameをKeyとしたHashを設定することができます。

>> params[:search][:name]
=> "なまえ"

url: users_path

urlにはinput type="submit"ボタンを押した時にどのURLにリクエストを送ればよいのかを指定します。
今回はUsersControllerのindexアクションにリクエストを送りたいのでusers_pathを指定します。
Rails - 名前付きルートにおける_pathと _urlの違いと使い分け

method: :get

検索はHTTPメソッドのGETにあたるのでmethod: :getを設定ます。
Railsガイド Web上のリソース

local: true

form_withではデフォルトでremote: trueとなっており、local: trueしない場合、XMLHttpRequest(Ajax)で通信が行われます。
XMLHttpRequestによって非同期通信を行うことができるのですが、今回は非同期通信をOFFにしたいので、local: trueにします。

By default form_with attaches the data-remote attribute submitting the form via an XMLHTTPRequest in the background if an Unobtrusive JavaScript driver, like rails-ujs, is used. See the :local option for more.

text_field(名前の入力欄)

value: @search_params[:name]とすることで検索パラメータを検索後も保持するようにしています。

<%= f.text_field :name, value: @search_params[:name] %>
# 初期表示時
<input type="text" name="search[name]" id="search_name">

# `なまえ`で検索した場合
<input value="なまえ" type="text" name="search[name]" id="search_name">

Ruby on Rails API - text_field

collection_radio_buttons(性別のラジオボタン選択)

collection_radio_buttonsでラジオボタンを作っています。
collection_radio_buttonsの実装を見てみるとこのようになっています。

def collection_radio_buttons(method, collection, value_method, text_method, options = {}, html_options = {}, &block)
  @template.collection_radio_buttons(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_html_options.merge(html_options), &block)
end

GitHub Rails - collection_radio_buttons

引数についてそれぞれ解説すると

  • method: params[:search][:method]のmethodに設定したいものを渡す。今回はgender
  • collection: ラジオボタンは1つだけでは意味がないので、ラジオボタンの項目のcollectionを設定する。今回はUser.genders
  • value_method: ラジオボタンのvalueに設定したい値をcollectionの要素に対するメソッドとして設定する。今回は:first
  • text_method: ラジオボタンのtextに設定したい値をcollectionの要素に対するメソッドとして設定する。今回は:first

collectionの要素に対するメソッド

Railsの内部では↓のようなことを行なっています。
スクリーンショット 2019-06-19 8.33.30.png

見た目を整えるためのコードなど余計な部分を省いたシンプルなコードがこちらです。

<%= f.collection_radio_buttons(:gender, User.genders, :first, :first) do |r| %>
  <!-- r.valueと@search_params[:gender]が一致した場合はcheckedにtrueを設定する-->
  <%= r.radio_button(checked: r.value == @search_params[:gender]) + r.text %>
<% end %>
<input type="hidden" name="search[gender]" value="" />
<input type="radio" value="unanswered" name="search[gender]" id="search_gender_unanswered" />unanswered
<input type="radio" value="male" name="search[gender]" id="search_gender_male" />male
<input type="radio" value="female" name="search[gender]" id="search_gender_female" />female
<input type="radio" value="other" name="search[gender]" id="search_gender_other" />other

date_field(誕生日の範囲指定)

誕生日が検索できるようにするためにdate_fieldを使います。また、誕生日はピンポイントで検索しても使いづらいので入力欄を2つ作り、範囲検索できるようにします。

<%= f.date_field :birthday_from, value: @search_params[:birthday_from] %>
<input type="date" name="search[birthday_from]" id="search_birthday_from">

今回の検索機能では問題ないですが、birthday_fromに設定される日付はString型(ハイフン区切りの文字列)であることは認識しておいた方がよいです。

スクリーンショット 2019-06-21 8.51.06.png

Ruby on Rails API - date_field

collection_select(都道府県のプルダウン選択)

都道府県の一覧をプルダウン選択できるようにします。

<%= f.collection_select(:prefecture_id, Prefecture.all, :id, :name, selected: @search_params[:prefecture_id], include_blank: '選択') %>
<select name="search[prefecture_id]" id="search_prefecture_id">
  <option value="">選択</option>
  <option value="1">北海道</option>
  <option value="2">青森県</option>
  省略
  <option value="46">鹿児島県</option>
  <option value="47">沖縄県</option>
</select>

基本的な使い方はcollection_radio_buttonsと同じです。

Ruby on Rails API - collection_select

I18n対応

I18n(Internationalization)とは、Railsアプリケーションを国際化対応させる機能です。
国際化対応と言われると難しそうに感じるかもしれませんが、ymlファイルに定義した言語ごとの文字を表示するだけです。

「国際化対応しないなら使わなくていいのでは?」と思うかもしれませんが、I18nを使うことで表記揺れ2を防ぐことにもなるので、日本限定のサービスでも使用するのが一般的です。(※筆者の観測範囲では)

Railsガイド Rails国際化 (I18n) API

インストール方法はこちらが参考になると思います。
i18nについて

使い方

検索機能に関係のある部分だけを抜粋したymlがこちらです。

config/locales/ja.yml
ja:
  activerecord:
    models:
      user: 'ユーザ'
    attributes:
      user:
        name: '名前'
        gender: '性別'
        birthday: '誕生日'
        prefecture_id: '都道府県'
      user/gender:
        unanswered: '未回答'
        male: '男性'
        female: '女性'
        other: 'その他'
  helpers:
    select:
      prompt: 選択してください
      include_blank: 選択
    submit:
      create: 登録する
      submit: 保存する
      update: 更新する
      search: 検索する
>> User.human_attribute_name(:gender)
=> "性別"
>> User.human_attribute_name("gender.male")
=> "男性"
>> t('helpers.select.include_blank')
=> "選択"

i18nのキーを調べる方法(nestされているパターン等)

Railsでgem無しに手軽にenumをi18nに対応させる

ラジオボタンを横並びにする(Bootstrap4)

Bootstrapを使ってラジオボタンを横並びにします。

公式
Bootstrap Ruby Gem

言葉で説明するよりもコードを見比べた方がわかりやすいと思うのでコードを貼っておきます。

# erb
<%= f.collection_radio_buttons(:gender, User.genders, :first, :first) do |r| %>
  <%= tag.div(class: 'form-check form-check-inline') do %>
    <%= r.radio_button(checked: r.value == @search_params[:gender], class: 'form-check-input') + f.label(User.human_attribute_name("gender.#{r.text}"), class: 'form-check-label') %>
  <% end %>
<% end %>
# 生成されたHTML
<input type="hidden" name="search[gender]" value="" />
<div class="form-check form-check-inline">
  <input class="form-check-input" type="radio" value="unanswered" name="search[gender]" id="search_gender_unanswered" />
  <label class="form-check-label" for="search_未回答">未回答</label>
</div>
<div class="form-check form-check-inline">
  <input class="form-check-input" type="radio" value="male" name="search[gender]" id="search_gender_male" />
  <label class="form-check-label" for="search_男性">男性</label>
</div>
<div class="form-check form-check-inline">
  <input class="form-check-input" type="radio" value="female" name="search[gender]" id="search_gender_female" />
  <label class="form-check-label" for="search_女性">女性</label>
</div>
<div class="form-check form-check-inline">
  <input class="form-check-input" type="radio" value="other" name="search[gender]" id="search_gender_other" />
  <label class="form-check-label" for="search_その他">その他</label>
</div>

Bootstrap移行ガイド - 横並び(Inline)から抜粋

<div class="form-check form-check-inline">
  <input class="form-check-input" type="checkbox" id="inlineCheckbox1" value="option1">
  <label class="form-check-label">1</label>
</div>
<div class="form-check form-check-inline">
  <input class="form-check-input" type="checkbox" id="inlineCheckbox2" value="option2">
  <label class="form-check-label">2</label>
</div>
<div class="form-check form-check-inline">
  <input class="form-check-input" type="checkbox" id="inlineCheckbox3" value="option3" disabled>
  <label class="form-check-label">3(無効)</label>
</div>

Controller

Controllerで行なっていることは以下の通りです。順番に解説していきます。

  1. 受け取った検索パラメータをチェックする(Strong Parameters)
  2. チェックした検索パラメータを保持する(インスタンス変数)
  3. 検索パラメータを元に検索する(Scope, includes)
  4. 検索結果を返す(インスタンス変数)

再掲

app/controllers/users_controller.rb
class UsersController < ApplicationController

  # indexアクション以外は不要なので省略しています。
  def index
    @search_params = user_search_params
    @users = User.search(@search_params).includes(:prefecture)
  end

  private

  def user_search_params
    params.fetch(:search, {}).permit(:name, :gender, :birthday_from, :birthday_to, :prefecture_id)
  end
end

1. 受け取った検索パラメータをチェックする(Strong Parameters)

パラメータを受け取る

検索パラメータの受け取り方ですが、params[:search]の中にKey, Valueのセットで検索パラメータが渡されるHash形式(厳密にはHashではなくActionController::Parametersオブジェクトです。)で受け取りたいと思います。

スクリーンショット 2019-06-02 15.41.38.png

params[:search]の中には{key: value}が{name: なまえ"}のHashが入っているので以下のように値を取り出すことができます。

>> params[:search][:name]
=> "なまえ"

パラメータをチェックする

RailsにはStrong Parametersという機能があります。Strong Parametersとは、システム側が意図していない値を入力された場合、サーバ側で無効化する仕組みです。悪意のあるユーザなどからの攻撃を防ぐため、受け取った値をサーバ側でチェックすることはセキュリティ上必須です。
Railsガイド(Strong Parameters)を確認してください。

Strong Parametersを利用して実装したメソッドがこちらです。

def user_search_params
  params.fetch(:search, {}).permit(:name, :gender, :birthday_from, :birthday_to, :prefecture_id)
end

fetchメソッド

params.fetch(:search, {})は、params[:search]が空の場合{}をparams[:search]が空でない場合、params[:search]を返してくれます。
Ruby on Rails API - fetch

空の場合
スクリーンショット 2019-06-02 16.10.28.png

空でない場合
スクリーンショット 2019-06-02 16.10.59.png

ちなみに、fetch(:search, {})require(:search)に書き換えるとparams[:search]が必須、つまり検索パラメータがない場合はActionController::ParameterMissingとなり、初期表示ができなくなります。

スクリーンショット 2019-06-02 16.29.24.png

permitメソッド

permitメソッドは許可していないKeyを切り捨てるメソッドです。user_search_paramsメソッドでは引数に指定した:name, :gender, :birthday_from, :birthday_to, :prefecture_idだけを許可して、他のKeyが渡された場合は無視します。
Ruby on Rails API - permit

例えばindex.html.erbでnameをnamuとタイポしていた場合、params[:search]ではnamuがありますが、user_search_paramsではnamuがなくなっています。

# app/views/users/index.html.erb
<%= f.text_field :namu, value: @search_params[:name] %>

スクリーンショット 2019-06-03 22.08.53.png

2. チェックした検索パラメータを保持する(インスタンス変数)

@search_params = user_search_paramsとしてControllerで宣言/代入することで、Viewで@search_paramsを使えるようになります。

3. 検索パラメータを元に検索する(Scope, includes)

検索はこの部分です。

User.search(@search_params).includes(:prefecture)

Userモデルのsearchメソッドを呼び出して(引数は@search_params)、結果には都道府県を含みます、と。
実装の詳細を確認せずとも内容が想像できますね。プログラミングで名前重要と言われる所以でもあります。

searchメソッドはRailsのScopeという仕組みで実装しています。解説はModelの方を参照してください。

includes

includesはN + 1問題に対応するために行なっています。includesすることでSQLの発行回数を少なくすることができます。

また、N + 1問題のN + 1とは以下の合計のことです。

  • @usersを取得するためにSQLを実行する(1)
  • @usersはprefecture_idを持っているけど、都道府県名は持っていないので、prefecture_idをKeyにSQLを実行する(N)

ユーザ一覧ページを初期表示した際の、SQLがこちらです。
(件数が多いと見辛いので、limit(5)しています。)

# User.search(@search_params).limit(5)
Started GET "/users" for 127.0.0.1 at 2019-06-03 22:54:28 +0900
Processing by UsersController#index as HTML
  Rendering users/index.html.erb within layouts/application
  Prefecture Load (0.5ms)  SELECT `prefectures`.* FROM `prefectures`
  ↳ app/views/users/index.html.erb:28
  User Load (0.6ms)  SELECT  `users`.* FROM `users` LIMIT 5
  ↳ app/views/users/index.html.erb:49
  Prefecture Load (1.6ms)  SELECT  `prefectures`.* FROM `prefectures` WHERE `prefectures`.`id` = 5 LIMIT 1
  ↳ app/views/users/index.html.erb:54
  Prefecture Load (0.5ms)  SELECT  `prefectures`.* FROM `prefectures` WHERE `prefectures`.`id` = 35 LIMIT 1
  ↳ app/views/users/index.html.erb:54
  Prefecture Load (0.4ms)  SELECT  `prefectures`.* FROM `prefectures` WHERE `prefectures`.`id` = 7 LIMIT 1
  ↳ app/views/users/index.html.erb:54
  Prefecture Load (0.5ms)  SELECT  `prefectures`.* FROM `prefectures` WHERE `prefectures`.`id` = 18 LIMIT 1
  ↳ app/views/users/index.html.erb:54
  Prefecture Load (0.4ms)  SELECT  `prefectures`.* FROM `prefectures` WHERE `prefectures`.`id` = 8 LIMIT 1
  ↳ app/views/users/index.html.erb:54
  Rendered users/index.html.erb within layouts/application (38.0ms)
Completed 200 OK in 188ms (Views: 180.7ms | ActiveRecord: 4.4ms)

# User.search(@search_params).includes(:prefecture).limit(5)
Started GET "/users" for 127.0.0.1 at 2019-06-03 22:55:45 +0900
Processing by UsersController#index as HTML
  Rendering users/index.html.erb within layouts/application
  Prefecture Load (0.7ms)  SELECT `prefectures`.* FROM `prefectures`
  ↳ app/views/users/index.html.erb:28
  User Load (0.6ms)  SELECT  `users`.* FROM `users` LIMIT 5
  ↳ app/views/users/index.html.erb:49
  Prefecture Load (3.4ms)  SELECT `prefectures`.* FROM `prefectures` WHERE `prefectures`.`id` IN (5, 35, 7, 18, 8)
  ↳ app/views/users/index.html.erb:49
  Rendered users/index.html.erb within layouts/application (211.5ms)
Completed 200 OK in 400ms (Views: 313.4ms | ActiveRecord: 61.7ms)

includesなしの方はSQLの発行が6回なのに対して、includesありの方はSQLの発行が2回になっています。
仮にユーザが10万人いる場合でもincludesありの方はSQLの発行が2回で済むため、パフォーマンスがよくなります。

4. 検索結果を返す(インスタンス変数)

@usersに検索結果を代入します。

Model

Modelで行なっていることは以下の通りです。順番に解説していきます。

  • Enumでgenderの値を定義する
  • Active Record Associations(関連付け)でUserとPrefectureの関係を定義する
  • Scopeで検索用のメソッドを定義する

再掲

app/models/user.rb
class User < ApplicationRecord
  enum gender: { unanswered: 0, male: 1, female: 2, other: 9 }

  belongs_to :prefecture

  scope :search, -> (search_params) do
    return if search_params.blank?

    name_like(search_params[:name])
      .gender_is(search_params[:gender])
      .birthday_from(search_params[:birthday_from])
      .birthday_to(search_params[:birthday_to])
      .prefecture_id_is(search_params[:prefecture_id])
  end
  scope :name_like, -> (name) { where('name LIKE ?', "%#{name}%") if name.present? }
  scope :gender_is, -> (gender) { where(gender: gender) if gender.present? }
  scope :birthday_from, -> (from) { where('? <= birthday', from) if from.present? }
  scope :birthday_to, -> (to) { where('birthday <= ?', to) if to.present? }
  scope :prefecture_id_is, -> (prefecture_id) { where(prefecture_id: prefecture_id) if prefecture_id.present? }
end

Enumでgenderの値を定義する

Enumは取りうる値の範囲が決まっている時にその値を定義することでRailsが便利な機能やメソッドを提供してくれる仕組みです。

例えば、Enumで定義していない値を設定しようとするとArgumentErrorになります。

>> User.new(gender: 3)
!! #<ArgumentError: '3' is not a valid gender>

他にも便利なメソッドが使えるようになります。

>> User.genders
=> {"unanswered"=>0, "male"=>1, "female"=>2, "other"=>9}

>> User.genders[:male]
=> 1

>> User.new(gender: 1).gender
=> "male"

>> User.new(gender: 1).male?
=> true

Ruby on Rails API - Enum
システムで「性別」の情報を扱う前に知っておくべきこと

Active Record Associations(関連付け)でUserとPrefectureの関係を定義する

belongs_to :prefectureとし、UserモデルとPrefectureモデルの関連付けを定義することで、user.prefecture.nameと書けるようになります。
(Viewで使っています。)

>> User.first.prefecture.name
=> "秋田県"

Scopeで検索用のメソッドを定義する

Scopeとは、ActiveRecordのQueryメソッドに名前を付ける機能です。戻り値はActiveRecord::Relationオブジェクトなので、ScopeからScopeを呼び出すことも可能です。

app/models/user.rb
class User < ApplicationRecord

  # 省略

  scope :search, -> (search_params) do
    # search_paramsが空の場合以降の処理を行わない。
    # >> {}.blank?
    # => true
    return if search_params.blank?

    # パラメータを指定して検索を実行する
    name_like(search_params[:name])
      .gender_is(search_params[:gender])
      .birthday_from(search_params[:birthday_from])
      .birthday_to(search_params[:birthday_to])
      .prefecture_id_is(search_params[:prefecture_id])
  end
  # nameが存在する場合、nameをlike検索する
  scope :name_like, -> (name) { where('name LIKE ?', "%#{name}%") if name.present? }
  # gender_isが存在する場合、gender_isで検索する
  scope :gender_is, -> (gender) { where(gender: gender) if gender.present? }
  # birthdayが存在する場合、birthdayで範囲検索する
  scope :birthday_from, -> (from) { where('? <= birthday', from) if from.present? }
  scope :birthday_to, -> (to) { where('birthday <= ?', to) if to.present? }
  # prefecture_idが存在する場合、prefecture_idで検索する
  scope :prefecture_id_is, -> (prefecture_id) { where(prefecture_id: prefecture_id) if prefecture_id.present? }
end

Railsガイド(Scope)
nil? empty? blank? present? の使い分け

この記事でやっていないこと

helperへの切り出し

collection_radio_buttonsが1行になるように切り出したいですよね。こんな感じで。

app/helpers/application_form_builder.rb
class ApplicationFormBuilder < ActionView::Helpers::FormBuilder
  include ActionView::Helpers::TagHelper
  include ActionView::Context

  def inline_radio_buttons(method:, collection:, checked:)
    collection_radio_buttons(method, collection, :first, :first) do |r|
      tag.div(class: 'form-check form-check-inline') do
        r.radio_button(checked: r.value == checked, class: 'form-check-input') + self.label(r.text, class: 'form-check-label')
      end
    end
  end
end

formオブジェクト

メドピア開発者ブログ form objectを使ってみよう

感想

普段自分が行なっていることを技術記事という形で言語化することで、深掘りできてよかったです。
今後勉強するときは「記事にできるくらいちゃんと理解しているか?」という視点を持ちつつ勉強していくぞい?('ω'?)


  1. あなたが作ろうとしている機能と似たような機能を、きっと他の誰かも作りたいと思っている。つまり、Railsが標準機能として提供している可能性が高い。 

  2. 例えば、プルダウンの先頭が、未選択選択選択してくださいのように実装箇所によって揺れてしまうこと。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

#Rails で has_many の association に対してネストしたハッシュを得る

こんな感じ。

Author.joins(:books).first.serializable_hash(include: :books)

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2215

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

EC2へcapistranoを使ってrailsアプリケーションをデプロイしている場合のログ確認

EC2でrailsアプリケーションをデプロイしています。
その際にページへアクセスした際、以下のようなエラーが、、

The page you were looking for doesn't exist.
You may have mistyped the address or the page may have moved.

If you are the application owner check the logs for more information.

railsがうまく動いていない時のエラーのようなので、
エラーログをみていきます。

見るべきログファイルの場所

アプリケーションディレクトリ内にあるcurrentというフォルダ

capistranoを使っている場合、ルートディレクトリはこのフォルダになるとのこと

アプリーケーション/current/log
このディレクトリに production.log unicorn.stderr というファイルがある。

これらのファイルを確認していく

logの確認コマンド

$ less production.log

↑ログの一番上から表示させる。

tail production.log

↑ログの末尾から表示させる。

tailf production.log

↑ログをリアルタイム更新で確認する

今回はtailfでproduction.logを確認した所、リアルタイムに動いていなかったため
それよりも前の段階でエラーが起きていると考えました。

次にunicorn.stderrを確認したい所だったのですが、
先にunicornが動いているか確認したと所動いていませんでした。

bundle exec cap production unicorn:start

で解決しました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails g ~ で指定のディレクトリ下にフォルダを格納したい場合

超基本ですが、一応メモってことで書いときます。

apiモードで作成する時、app/controllers/api/v1の下にuserコントローラーを置きたい時は、

terminal
$ rails g controller api::v1::users index show new create edit update destroy

これで、

app/controllers/api/v1/user_controller.rb # これが作成される

ルーティングは、こうですね。

config/routes.rb
namespace 'api' do
 namespace 'v1' do
  resources :users
 end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails g ~ でapp/controllers/api/v1みたいな感じでファイルを格納したい場合

超基本ですが、一応メモってことで書いときます。

apiモードで作成する時、app/controllers/api/v1の下にuserコントローラーを置きたい時は、

terminal
$ rails g controller api::v1::users index show new create edit update destroy

これで、

app/controllers/api/v1/user_controller.rb # これが作成される

ルーティングは、こうですね。

config/routes.rb
namespace 'api' do
 namespace 'v1' do
  resources :users
 end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RubyのバージョンアップをしたらHerokuにデプロイできなくなった

tl;dr

個人でつくってるプロダクトで

ふと思い立って、Rubyのバージョンアップを行ったら

Herokuにデプロイできなくなった

それの解決方法というか、、

原因

Rubyのバージョンアップの際に、gem install bundler をして
Bundlerのバージョンが2.0.2 となったからっぽい

$ bundle -v
Bundler version 2.0.2
Gemfile.lock
BUNDLED WITH
   2.0.2

Bundlerを2.0.1で入れ直して、 Gemfile.lock もつくりなおす

$ gem uninstall bundler
$ gem install bundler -v 2.0.1

$ bundle -v  
Bundler version 2.0.1
$ rm Gemfile.lock
$ bundle --without production

これでできる

$ git push heroku master

おまけ

バージョンアップしたらRailsコマンドが叩けなくなった

rbenv: rails: command not found

The `rails' command exists in these Ruby versions:
$ rbenv exec gem install bundler

$ rbenv rehash

$ gem install rails 
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails で名前付きルーティングと通常ルーティングを併用したいとき

はじめに

Rails の URL で、通常の /hoge/:id のようなルーティングと /hoge/fuga のような名前付きルーティングを併用したいという課題がありました。

その際どのように対応したのかを備忘として残しておきます。

前提環境

  • Rails 5.2.2.1

サンプル例

name カラムのある Post モデルを例にします。

このモデルの name カラムに文字列が入っていれば、id ではなく name の文字列を URL として使うことを想定します。

  • name が空 → /posts/1
  • namehoge と入っている → /posts/hoge

方針

方針としては、Rails ガイド を参考に、以下のように進めました。

  • routes で利用するパラメータ識別子修正
  • ActiveRecord::Base#to_param をオーバーライド
  • 名前付きルーティング・通常ルーティングどちらでもレコードを find できるメソッド作成

対応

routes で利用するパラメータ識別子修正

まず、routes のパラメータ識別子を修正します。param オプションを使うことで可能です。

idname で find したかったので、id_or_name というパラメータ名にしています。

Rails.application.routes.draw do
  # 中略
  resources :posts, param: :id_or_name
  # 中略
end

ActiveRecord::Base#to_param をオーバーライド

次に、ActiveRecord::Base#to_param をオーバーライドします。

name があれば name を、存在しなければそのまま id を利用する形としました。

class Post < ApplicationRecord
  # 中略
  def to_param
    return name if name.presence
    super
  end
end

名前付きルーティング・通常ルーティングどちらでもレコードを find できるメソッド作成

最後に、Post モデルに名前付きルーティング・通常ルーティングどちらでもレコードを find できるメソッドを追加します。

idname でレコードを検索し、どちらかがマッチしていればそのレコードを返却します。
存在しない場合は一応 ActiveRecord::RecordNotFound で例外処理しています。

class Post < ApplicationRecord
  # 中略
  def self.find_by_id_or_name(id_or_name)
    if object = self.where(id: id_or_name).or(self.where(slug: id_or_name)).first
      object
    else
      raise ActiveRecord::RecordNotFound
    end
  end
end

利用するときは以下のようなメソッドの呼び出し方となります。

Post.find_by_id_or_name(params[:id_or_name])

終わりに

name 重複する場合など、考慮する点はまだあるかも知れませんが、一旦こちらで対応はできました。

もっと良いやり方などありましたらコメントいただけるとありがたいです!

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails ~会員登録~

これまでのあらすじ

  1. 仮登録でメールアドレスを登録
  2. 登録したメールアドレスへ本登録画面に飛ぶURLを記載したメールを送信
  3. メールでURLへ遷移
  4. 登録に必要な情報の入力
  5. 登録完了
  6. ログイン
  7. ログアウト

までやっていきたいと思います。


メール送信

前の記事でユーザ仮登録のモーダルウィンドウ表示まで実装しました。
で、登録ボタンで以下のアクションを呼びます。

top_controller.rb
  def create
    @temp_user = TempUser.create_temp_user(temp_users_params)
    respond_to do |format|
      if @temp_user.save
        UserMailer.with(temp_user: @temp_user, locale: params[:locale]).request_registration.deliver_later
        format.js { @status = "success" }
      else
        format.js { @status = "fail" }
      end
    end
  end

TempUserクラスの#create_temp_userメソッドで初期化します。

temp_user.rb
    # 仮ユーザの作成
    def create_temp_user(params)
      # temp_usersにmail_addressで検索 初期化する
      temp_user = find_or_initialize_by(mail_address: params[:mail_address])
      temp_user.last_name = params[:last_name]
      temp_user.first_name = params[:first_name]
      temp_user.token = create_token
      temp_user.expired_at = DateTime.now + 1
      return temp_user
    end

find_or_initialize_byでメールアドレスを検索します。
もし、仮登録済みで本登録していないメールアドレスの場合、後勝ちにして最後に入力した情報でUPDATEします。
もし、temp_userにない場合は、入力情報でINSERTします。
find_or_initialize_byを使用し、saveをするタイミングでINSERTかUPDATEか判定、UPSERTが実現できます。

expired_at(有効期限)は、とりあえず1日にしていますが、設定を外出ししたいですね。

saveメソッドの戻り値でSQLの成功・失敗を判定して処理を判定しています。

成功時は、登録情報を元にUserMailerでメールを作成しています。

user_mailer.rb
class UserMailer < ApplicationMailer
  default from: "hogehoge@gmail.com"

  def request_registration
    @temp_user = params[:temp_user]
    @locale = params[:locale]
    mail(to: @temp_user.mail_address, subject: I18n.t("mailers.user_mailer.request_registration.subject"))
  end
end

ApplicationMailerを継承したUserMailerです。
from:は送信元のメールアドレスを設定します。
request_registrationがメール送信の本体です。
paramsで引数を受け取ってメールを生成します。

メール本体は、viewsの下に作ります。

request_registration.html.erb
<%= stylesheet_link_tag "mailers/request_registration.css", media: "all" %>
<p class="message"><%= t('mailers.user_mailer.request_registration.dear', last_name: @temp_user.last_name, first_name: @temp_user.first_name) %></p>
<pre class="message"><%= t('mailers.user_mailer.request_registration.message_text', expired_at: l(@temp_user.expired_at, format: :default)) %></pre>
<div class="btn">
  <%= link_to t('mailers.user_mailer.request_registration.button'), {controller: 'account', action: 'regist', locale: @locale, token: @temp_user.token } %>
</div>

メソッド名のhtml.erb(HTMLメール)またはtext.erb(テキストメール)を雛形として作ります。
上記は、HTMLメールの雛形です。link_toでaccount_controller.rbのregistメソッドを指定しています。temp_user登録の際、生成したtokenをGETリクエストをパラメタとしてURLに付与し、link_toで生成しています。


メールを受信する

開発中に動作確認したいですが、実際に送ると、誤送信する恐れがあるので、以下のgemを入れます。

Gemfile
  # letter_opener_web
  gem 'letter_opener_web', '~> 1.0'

このgemはメールを送信・受信してくれます。
config/environments/development.rbに設定を追加します。

development.rb
config.action_mailer.delivery_method = :letter_opener_web

また、routes.rbに以下のパスを設定し、送信メールを確認できるようにします。

routes.rb
    # letter_opener_web
    mount LetterOpenerWeb::Engine, at: "/letter_opener" if Rails.env.development?

これで(http://localhost:3000/letter_opener) でアプリが送信したメールをブラウザで確認できます。


登録

メールに記載されたURLを押下すると、登録に必要な情報を入力する画面へ遷移できます。

パスワードについて

ライブラリを使わないでログイン機能を実装するために以下のgemを使います。

Gemfile
# Use ActiveModel has_secure_password
gem 'bcrypt', '~> 3.1.7'

Userモデルにhas_secure_passwordを追加します。

user.rb
class User < ApplicationRecord
  include ActiveModel::Validations
  has_secure_password validations: true

  validates :last_name, presence: true
  validates :first_name, presence: true
  validates :last_name_roman, upper_case_format: true, unless: Proc.new { |p| p.last_name_roman.blank? }
  validates :first_name_roman, upper_case_format: true, unless: Proc.new { |p| p.first_name_roman.blank? }
  validates :sex, inclusion: { in: [0, 1] }
  validates :birthed_on, presence: true
  validates :mail_address, uniqueness: true
  validates :password, password_format: true
  validate :already_used_mail_address

  # already used mail address
  def already_used_mail_address
    unless User.find_by(mail_address: mail_address).nil?
      errors.add(:mail_address, I18n.t("validate.already_use"))
    end
  end

  # create remember token
  def self.create_remember_token
    SecureRandom.urlsafe_base64
  end

  # encrypt
  def self.encrypt(token)
    Digest::SHA256.hexdigest(token.to_s)
  end
end

passwordとpassword_confirmation属性、さらにauthenticateメソッドが使用できるようになります。
さらにDB内ではpassword_digestというカラムで暗号化されたパスワードは保存されます。
(password_digestをmigrationで対象のテーブルに追加します。)

アプリケーションログには、入力値は当然出力されず
DBには、暗号化されたパスワード文字列が登録されます。

登録ボタンでcreateメソッドが呼び出され、登録成功したら完了画面、失敗したら再度登録画面をレンダリングします。

account_controller.rb
  def create
    @user = User.new(users_params)
    if @user.save
      render action: :complete
    else
      render action: :regist
    end
  end

ログイン・ログアウト

application_controller.rbに以下を定義します。

application_controller.rb
class ApplicationController < ActionController::Base

  # filter
  # actionの直前に実行されるfilter
  before_action :set_locale
  before_action :current_user
  before_action :require_sign_in!
  # helper methodとして使用できる
  helper_method :signed_in?

  def set_locale
    I18n.locale = locale
  end

  def locale
    @locale ||= params[:locale] ||= I18n.default_locale
  end

  def default_url_options(options = {})
    options.merge(locale: locale)
  end

  def current_user
    remember_token = User.encrypt(cookies[:remember_token])
    @current_user ||= User.find_by(remember_token: remember_token)
  end

  def sign_in(user)
    remember_token = User.create_remember_token
    # cookieにremember_tokenをsetする
    cookies.permanent[:remember_token] = remember_token
    # remember_tokenを更新する
    user.update_column(:remember_token, User.encrypt(remember_token))

    @current_user = user
  end

  def sign_out
    @current_user = nil
    # cookieのremember_tokenを削除する
    cookies.delete(:remember_token)
    redirect_to login_path
  end

  def signed_in?
    @current_user.present?
  end

  private

  def require_sign_in!
    redirect_to login_path unless signed_in?
  end
end

filter処理として
current_userメソッド
ログイン時にCookieとDBに登録したトークン情報を突き合わせて、ユーザ情報を取得する
require_sign_in!メソッド
ユーザ情報が取得できない場合(=ログインしていない場合)ログイン画面へリダイレクトする

これをapplication_controller.rbに定義することによりapplication_controllerを継承する全てのcontrollerにこのfilter処理が走ります。

これにより画面上でログイン状態・非ログイン状態を判断するわけです。

ログイン処理はsession_controller.rbに定義しています。

session_controller.rb
class SessionsController < ApplicationController
  # filter
  # actionの直前に実行されるfilterをskipする
  skip_before_action :require_sign_in!, only: [:new, :create]
  # actionの直前に実行されるfilter
  before_action :set_user, only: [:create]

  # GET /login
  def new
    redirect_to root_path
  end

  # PUT /login
  def create
    if @user.authenticate(@session.password)
      sign_in(@user)
    else
      @session.sign_in_failure
    end
    render "top/index"
  end

  # DELETE /logout
  def destroy
    sign_out
    redirect_to login_path
  end

  private

  def set_user
    @session = Session.new(session_params)
    if !@session.valid?
      render "top/index" and return
    end
    @user = User.find_by!(mail_address: @session.mail_address)
  rescue
    @session.sign_in_failure
    render "top/index"
  end

  def session_params
    params.require(:session).permit(:mail_address, :password)
  end
end

Sessionモデルはmail_addressとpasswordのログイン認証に必要な情報をもっているクラスです。

skip_before_actionは、actionの直前に実行されるfilterをskipするための宣言です。
当然ログイン画面表示とログイン処理にログイン認証のfilterが入っているとログインできないため、skipしています。

createメソッドの前のみset_userのfilterが適用されます。

set_userメソッドは画面から入力されたフォーム情報を元にmail_addressでユーザ情報を取得します。
取得できない場合、例外を発生させます。例外発生時はログイン画面をレンダリングします。

createメソッドは、set_userメソッドで取得したユーザ情報とパスワードを確認します。
確認できた場合、ログイン処理(application_controller.rbに定義)をします。

ログイン処理は、ログイン認証していることを示す、remember_tokenを画面に保持します。
remember_tokenはUserクラスで定義したメソッドでランダム文字列で生成されます。
それをCookieに保存し、その後DB(userテーブル)UPDATEします。

前述のログイン確認のfilterはこのremember_tokenを元にログインしているか、していないかを判断するというわけです。


終わりに

  • 勉強会の資料なので、急いで作ったので、もうちょっと修正します。
  • 参考リンクとかも記載せねば…

参考

工事中…

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ActiveSupport::TimeZoneを0オフセットでインスタンス化すると1時間ずれる

指定したオフセット値(JSTなら9時間)で ActiveSupport::TimeZone を作り、それをもとに ActiveSupport::TimeWithZone インスタンスを作りたい。

jst = ActiveSupport::TimeZone[9]
# => #<ActiveSupport::TimeZone:0x00007fbbe3b034b0 @name="Osaka", @utc_offset=nil, @tzinfo=#<TZInfo::TimezoneProxy: Asia/Tokyo>>

jst.now
# => Fri, 21 Jun 2019 21:24:26 JST +09:00

+9は、問題なし。

utc = ActiveSupport::TimeZone[0]
# => #<ActiveSupport::TimeZone:0x00007fbbe3afb030 @name="Casablanca", @utc_offset=nil, @tzinfo=#<TZInfo::TimezoneProxy: Africa/Casablanca>>

utc.now
# => Fri, 21 Jun 2019 13:25:48 WEST +01:00

utc.tzinfo.current_period.utc_offset                      
# => 0

ファッ!? +00:00 が期待されるところ +01:00 となっている!?
TZInfoのutc_offsetは0を指しているようだが...

そもそもoffsetだけではtimezoneと言えないのでやろうとしていることが強引な気もするが、それはさておき、どうやるのが正解なのだろうか。


関係ありそうだが、詳しくは読んでない。
https://github.com/tzinfo/tzinfo/issues/98


動作確認環境
ruby 2.5.0p0 (2017-12-25 revision 61468) [x86_64-darwin16]
activesupport (5.2.1)
tzinfo (1.2.5)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsの起動エラーとMySQLのエラー解除方法(個人メモ)

Railsの起動エラーと、MySQLのエラー解除方法を個人的にメモする。
【PC:MacBookPro2012(13-inch, Mid 2012)、MacOS Mojave 10.14.4】
【ruby 2.3.7p456 (2018-03-28 revision 63024) [universal.x86_64-darwin18]】
【Rails 5.2.3】
【mysql Ver 8.0.15 for osx10.14 on x86_64 (Homebrew)】

MySQLのエラーについて。

まずはmysqlの場所を確認。

$ which mysql
/usr/local/bin/mysql

MySQLサーバーを起動させる。

$ mysql.server start
Starting MySQL
...... ERROR! The server quit without updating PID file (/usr/local/var/mysql/*****.local.pid).

PIDファイルを確認する。

$ ls -la /usr/local/var/mysql/*****.local.pid
ls: /usr/local/var/mysql/*****.local.pid: No such file or directory

MySQLサーバーをストップしてみる。

$ mysql.server stop
ERROR! MySQL server PID file could not be found!

sudoを入れて起動してみる。

$ sudo mysql.server start
Password:
Starting MySQL
. ERROR! The server quit without updating PID file (/usr/local/var/mysql/*****.local.pid).

安全にMySQLを起動してみる。

$ mysqld_safe
2019-04-24T03:34:24.6NZ mysqld_safe Logging to '/usr/local/var/mysql/*****.local.err'.
2019-04-24T03:34:24.6NZ mysqld_safe Starting mysqld daemon with databases from /usr/local/var/mysql
2019-04-24T03:34:29.6NZ mysqld_safe mysqld from pid file /usr/local/var/mysql/*****.local.pid ended

とりあえずアンインストールしてみる。

$ brew uninstall mysql
Uninstalling /usr/local/Cellar/mysql/8.0.15... (267 files, 234.6MB)

ちゃんと削除できたか確認。

$ which mysql
(何も出てこない)

再度インストールしてみる。

$ brew install mysql

これでmysql.server startしてもエラーだったので、上の操作を繰り返してみたけどダメ。

「PIDファイルが見つからない」ので、ファイルの生成をしてみる。

$ /usr/local/var/mysql/mysql > touch /usr/local/var/mysql/*****.local.pid
-bash: /usr/local/var/mysql/mysql: is a directory

奇跡的に起動しないか願いを込めて。

$ mysql.server start
Starting MySQL
...... ERROR! The server quit without updating PID file (/usr/local/var/mysql/*****.local.pid).

どこかで見つけたこのコマンドを入力してみる。

$ /usr/local/var/mysql/mysql > sudo mysql.server restart
-bash: /usr/local/var/mysql/mysql: is a directory

同じかい!!(ノ_<)

$ sudo chown -R _mysql:_mysql /usr/local/var/mysql /tmp/mysql.sock
Password:

何やらパスワードを求められた。。。いけるのか?

sudoで起動させてみよう。

$ sudo mysql.server start
Starting MySQL
..... ERROR! The server quit without updating PID file (/usr/local/var/mysql/*****.local.pid).

あかんのかい!(T . T)

sudoなしで起動できるようにするらしいコマンド。

$ sudo chown -R `whoami`:admin /usr/local/var/mysql /tmp/mysql.sock

起動させてみる。

$ mysql.server start
Starting MySQL
.. SUCCESS! 

キターーーーーーーーー!!!!!!♪───O(≧∇≦)O────♪
よーし!早速Railsをローカル環境で表示させてみよう。

Railsのシステムを起動。

$ rails s

ブラウザでlocalhost:3000を入力。(成功なら画面にRailsの初期画面が表示される)

なんとエラー!!恐怖の赤い画面!!
ActiveRecord::NoDatabaseError (Unknown database 'hello_sample7_development'):
え、データベースがない・・・?∑(゚Д゚)

ということでデータベースを作成。

$ rails db:create
Created database 'ファイル名_development'
Created database 'ファイル名_test'

再度Railsのシステム起動。

$ rails s

Railsの初期画面が出た!!!☆:.。. o(≧▽≦)o .。.:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails+WebpackerでVue.jsとTurbolinksを同時に動かす

これは何

RailsでVue.jsとTurbolinks動かしていい感じのフロントエンド環境を構築しよう!!!

一応それぞれのざっくりとした説明。

  • Rails
    • みんな大好きWebフレームワーク
  • Webpacker
    • Railsで書いたJSやCSSをよしなにまとめてくれるやつ
  • Vue.js
    • 言わずと知れたJSのフロントエンドフレームワーク
  • Turbolinks
    • ページ移動を速くしてくれるやつ

リポジトリとバージョン情報

リポジトリ: https://github.com/rhistoba/rails_vuejs_turbolinks_template

  • Ruby: 2.6.3
  • Rails: 5.2.3
  • Webpacker: 4.0.7
  • Vue.js: 2.6.10
  • Turbolinks: 5.2.0

どういう感じにVue.jsを使えるようにするのか

  1. Railsアクションからビューをレンダリング
  2. turbolinks:load時にビューからVueインスンタンスのid要素を検索
  3. id要素が見つかればVueインスタンスを生成
  4. ビューのid要素以下をテンプレートとしてVueインスタンスが適用される

こんな感じで、ビューでidを指定した箇所にVueを適用するための方法を説明します。
部分的にVueを適用可能なので、いわゆる薄い使い方によりRails Wayから外れない開発が可能かと思います。

手順

今回は適当にrails newしてルートのビューだけ作成したRailsプロジェクトを対象に説明します。

Webpackerを導入

GemfileにWebpackerを追加。

Gemfile
# ...
gem 'webpacker', '~> 4.x'
# ...

追加したらbundle install

$ bundle install

以下のコマンドでプロジェクトにWebpackerをインストールする。

$ bundle exec rails webpacker:install

Vue.jsを導入

以下のコマンドでプロジェクトにVue.jsを追加する。

$ bundle exec rails webpacker:install:vue

Turbolinksなどの導入

yarnで以下のようにパッケージを取得する。

$ yarn add turbolinks vue-turbolinks

javascript/packs/application.jsに以下を追加する。

javascript/packs/application.js
import Turbolinks from 'turbolinks'

Turbolinks.start()

views/layouts/application.html.erbで以下の一文を追加する。

views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    ...
+   <%= javascript_pack_tag 'application' %>
  </head>
  ...
</html>

これでWebpackerによりjavascripts/packs/application.jsがビルドされるようになる。

Vue.jsで完全ビルドを有効にする

ビューから取得したid要素のhtmlを、Vueインスタンスにテンプレートとして渡してコンパイルされる必要があるのですが、Vueは標準でランタイム限定ビルドのみ有効になっており、このままでは意図通りに動かせません。
参考: https://jp.vuejs.org/v2/guide/installation.html#さまざまなビルドについて

そのため参考URLのページにも載っているように、Webpackの設定で完全ビルドを有効にします。

config/webpackvue.config.jsを作成します。

config/webpack/vue.config.js
module.exports = {
  resolve: {
    alias: {
      'vue$': 'vue/dist/vue.esm.js'
    }
  }
}

config/webpack以下の環境ごとの設定ファイルに上記の設定を取り込むようにします。
(以下はdevelopment.jsの例)

config/webpack/development.js
process.env.NODE_ENV = process.env.NODE_ENV || 'development'

const environment = require('./environment')
const config = Object.assign(environment.toWebpackConfig(), require('./vue.config'))

module.exports = config

environment.toWebpackConfig()で生成される設定オブジェクトをObject.assign()を用いて上書きしています。

src/main.jsを作成

javascripts/packs/application.jsで各種Vueインスタンスを読み込む大元のファイルを作成しましょう。

javascripts以下にsrcディレクトリを作成します。

mkdir javascripts/src

javascripts/src以下にmain.jsを以下の内容で作成します。

main.js
import Vue from 'vue'
import TurbolinksAdapter from 'vue-turbolinks'

Vue.use(TurbolinksAdapter)

実際に使う

準備ができたので、実際にVueインスタンスを作成して動かします。

適当なビュー(今回はviews/home/index.html.erb)でVueインスタンスで使われるid要素を追加して、その要素以下でVueのテンプレート表記でhtmlを記述します。

views/home/index.html.erb
<div id="vue-app">
  {{message}}
</div>

javascripts/src以下にVueインスタンス作成のファイルを書きます。

javascripts/src/app.js
import Vue from 'vue'

document.addEventListener('turbolinks:load', () => {
  const el = document.getElementById('vue-app')
  if (el) {
    new Vue({
      el: el,
      data() {
        return {
          message: 'Hello Vue!'
        }
      }
    })
  }
})

最後にjavascripts/src/main.jsで上記のファイルをimportするよう追記します。

javascripts/src/main.js
import Vue from 'vue'
import TurbolinksAdapter from 'vue-turbolinks'

Vue.use(TurbolinksAdapter)

+ import './app.js'

rails sしてブラウザで以下のように確認できれば、完了です。

RailsVueTemplate.png

おしまい

RailsでのVueライフをごゆるりと…

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む