らくだ🐫にもできるRailsチュートリアル|14.2

14.2 [Follow] のWebインターフェイス

フォロー/フォロー解除の基本的なインターフェイスを実装し
フォローしているユーザーとフォロワーにそれぞれ表示用のページを作成する!

14.2.1 フォローのサンプルデータ

rails db:seedを使ってDBにサンプルデータを登録する
先にサンプルデータを自動作成できるようにしておくことにより
WEBページの見た目から先に取り掛かることが出来、バックエンド機能の実装を後に回すことが出来る

・
・
・
# リレーションシップのサンプルを追加
# usersにすべてのユーザーを代入
users = User.all
# userにUserテーブルの1番目のユーザーを代入
user  = users.first
# followingにusersの3番目~51番目を代入
following = users[2..50]
# followersにusersの4番目~41番目を代入
followers = users[3..40]
# followingを順に取り出してブロック内を実行
# 取り出した要素をfollowedに代入 userがfollowedをフォロー
following.each { |followed| user.follow(followed) }
# followersを順に取り出してブロック内を実行
# 取り出した要素をfollowerに代入 followerがユーザーをフォロー
followers.each { |follower| follower.follow(user) }
  • userが3番目~51番目のユーザーをフォロー
  • 4番目~41番目のユーザーがuserをフォロー
$ rails db:migrate:reset
$ rails db:seed

準備完了!

演習

  1. コンソールを開き、User.first.followers.countの結果がリスト 14.14で期待している結果と合致していることを確認してみましょう。
  2. 先ほどの演習と同様に、User.first.following.countの結果も合致していることを確認してみましょう。
# User.firstがフォローされている数
>> User.first.followers.count
  User Load (0.1ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
   (0.2ms)  SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ?  [["followed_id", 1]]
=> 38
# User.firstがフォローしている数
>> User.first.following.count
  User Load (0.1ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
   (0.2ms)  SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ?  [["follower_id", 1]]
=> 49

1も2も期待した通りの結果になっている
seedで作ったデータは(userに代入した)User.firstがフォローするほう(following)が先で
フォローされるほう(followers)が後だから
演習結果のカウントを見ると一瞬🤔ってなるね(コードを見ろと言われたらその通りなんだけど)

14.2.2 統計と [Follow] フォーム

サンプルユーザーにフォロイーとフォロワーが出来たので
プロフィールページとHomeページを更新してこれらが反映されるようにしていく!

  1. フォロイーとフォロワーの統計情報を表示するパーシャルを作る
  2. フォロー用とフォロー解除用のフォームを作る
  3. フォロイーとフォロワーそれぞれの一覧ページを作る

Twitterの慣習に倣ってフォロー数の単位には「following」を使うって事で参考画像も出てるけど
日本語ページだと「フォロー」「フォロワー」で出てるのでそうしようと思います

統計情報には、現在のユーザーがフォローしている人数と、現在のフォロワーの人数が表示されています。それぞれの表示はリンクになっており、専用の表示ページに移動できます。第5章では、これらのリンクはダミーテキスト’#’を使って無効にしていました。しかしルーティングについての知識もだいぶ増えてきたので、今回は実装することにしましょう

実際のページは次のセクションで作成するんだけどルーティングは先に実装しちゃうとの事

・
・
・
  # resources :usersにネストを追加
  resources :users do
    # 以下でユーザーidが含まれるURLを扱えるようになる
    member do
      # :following, :followersのgetのルーティングを生成 
      get :following, :followers
    end
  end
  # account_activationsresourceのeditへのルーティングのみを生成
  resources :account_activations, only: [:edit]
  # password_resetsのnew、create、edit、updateのルーティングを生成 
  resources :password_resets,     only: [:new, :create, :edit, :update]
  # micropostsのcreateとdestroyのルーティングを生成
  resources :microposts,          only: [:create, :destroy]

end

これにより/users/1/followingや/users/1/followersといったURL
following_user_path(1)やfollowers_user_path(1)といった名前付きルートが使えるようになる
RoRT本文参照

ルーティングのネスト


ルーティングをネストすることで紐付いているモデルの親子関係を表すことが出来るようになる

統計情報を表示するパーシャルを作成&表示

<!--@userに @user もしくは@userがnilの場合current_user を代入-->
<!--プロフィールページとHomeページの両方に表示させるため現在のユーザーを取得-->
<% @user ||= current_user %>
<div class="stats">
  <a href="<%= following_user_path(@user) %>">
    <strong id="following" class="stat">
      <!--@userのfollowing数を(DB内で)count-->
      <%= @user.following.count %>
    </strong>
    <%= t('.following') %>
  </a>
  <a href="<%= followers_user_path(@user) %>">
    <strong id="followers" class="stat">
      <!--@userのfollowers数を(DB内で)count-->
      <%= @user.followers.count %>
    </strong>
    <%= t('.followers') %>
  </a>
</div>

ハイライト行、
CSS idを指定しているのは、14.2.5でAjaxを実装するときに便利な為のと事

<div class="row">
  <aside class="col-md-4">
    <section class="user_info">
      <%= render 'shared/user_info' %>
    </section>
    <section class="stats">
      <%= render 'shared/stats' %>
    </section>
    <section class="micropost_form">
      <%= render 'shared/micropost_form' %>
    </section>
  </aside>
    <div class="col-md-8">
      <h3>Micropost Feed</h3>
      <%= render 'shared/feed' %>
    </div>
</div>

出来上がったパーシャルが表示されるようにrenderで呼び出すんだけど
本文はhome.html.erbに追加してるけど
どこかでhome.html.erbの内容もログインしてるかしてないかのif文でパーシャルを呼び出すようにしているので
ログインしている際に呼び出される_user_logged_in.html.erbに該当コードを追加する

・
・
・
/* sidebar */
・
・
・
.gravatar_edit {
  margin-top: 15px;
}

.stats {
  overflow: auto;
  margin-top: 0;
  padding: 0;
  a {
    float: left;
    padding: 0 10px;
    border-left: 1px solid $gray-lighter;
    color: gray;
    &:first-child {
      padding-left: 0;
      border: 0;
    }
    &:hover {
      text-decoration: none;
      color: blue;
    }
  }
  strong {
    display: block;
  }
}

.user_avatars {
  overflow: auto;
  margin-top: 10px;
  .gravatar {
    margin: 1px 1px;
  }
  a {
    padding: 0;
  }
}

.users.follow {
  padding: 0;

/* forms */
・
・
・

スタイルも追加(14章で使うすべてのスタイルを追加しているので、まだ出てきてない属性もあるけどキニシナイ!!)

フォロー/フォロー解除フォームのパーシャル

<!--@userがログインユーザー(current_user)でなければブロック内を表示-->
<% unless current_user?(@user) %>
  <div id="follow_form">
  <!--@userをログインユーザー(current_user)がフォローしていたらブロック内を表示-->
  <% if current_user.following?(@user) %>
    <!--unfollowのパーシャルを呼び出し-->
    <%= render 'unfollow' %>
  <!--そうでなければ-->
  <% else %>
    <!--followのパーシャルを表示-->
    <%= render 'follow' %>
  <% end %>
  </div>
<% end %>

↑これはfollowとunfollowのパーシャルに表示を分けているだけ
自分(ログインユーザー=current_user)に対してはフォロー・フォロー解除は存在しないので
unless current_user?(@user)の時のみブロック内を表示する
また、それぞれのパーシャルではRelationshipsリソース用の新しいルーティングが必要な為
routes.rbにコードを追加

・
・
・
  # account_activationsresourceのeditへのルーティングのみを生成
  resources :account_activations, only: [:edit]
  # password_resetsのnew、create、edit、updateのルーティングを生成 
  resources :password_resets,     only: [:new, :create, :edit, :update]
  # micropostsのcreateとdestroyのルーティングを生成
  resources :microposts,          only: [:create, :destroy]
  # relationshipsのcreateとdestroyのルーティングを生成
  resources :relationships,       only: [:create, :destroy]
end

ユーザーをフォローするフォームとフォロー解除するフォームでは
どちらもform_forを使ってRelationshipモデルオブジェクトを操作する
ユーザーをフォローするフォームでは新しいリレーションシップを作成するため
POSTリクエストをRelationshipsコントローラに送信してcreate (作成) する

<!--current_user(ログイン中のユーザー)と結び付けた新しいRelationshipオブジェクトを返すフォーム-->
<%= form_for(current_user.active_relationships.build) do |f| %>
  <!--followed_idに@user.idを渡す-->
  <div><%= hidden_field_tag :followed_id, @user.id %></div>
  <%= f.submit t('.follow_btn'), class: "btn btn-primary" %>
<% end %>

ユーザーのフォローを解除するフォームでは既存のリレーションシップを見つけ出し
DELETEリクエストを送信してリレーションシップをdestroy (削除) する

<!--current_user(ログイン中のユーザー)と結び付いたリレーションシップからfollowed_idが @user.idのデータを取得-->
<!--deleteメソッドを指定-->
<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id),
             html: { method: :delete }) do |f| %>
  <%= f.submit t('.unfollow_btn'), class: "btn" %>
<% end %>

最終的に、このフォロー/フォロー解除フォームにはボタンしかないことを理解していただけたと思います。しかし、それでもこのフォームはfollowed_idをコントローラに送信する必要があります。これを行うために、リスト 14.21のhidden_field_tagメソッドを使います。

hidden_field_tagメソッドが次のようなフォーム用のHTMLを生成してくれる

<input id="followed_id" name="followed_id" type="hidden" value="3" />

パスワードの再設定フォームの時のように
隠しフィールドのinputタグを使うことで、ブラウザ上に表示させずに適切な情報を含めることが出来る!

これらをまとめたプロフィール画面がこれ↓

<% provide(:title, @user.name) %>
<div class="row">
  <aside class="col-md-4">
    <section class="user_info">
      <h1>
        <%= gravatar_for @user %>
        <%= @user.name %>
      </h1>
    </section>
    <!--フォロー中・フォロワーの統計情報を表示-->
    <section class="stats">
      <%= render 'shared/stats' %>
    </section>
  </aside>
  <div class="col-md-8">
    <!--フォロー・フォロー解除のフォーム(ボタン)を表示-->
    <%= render 'follow_form' if logged_in? %>
    <!-- @user.micropostsがあったら表示する-->
    <% if @user.microposts.any? %>
      <h3><%= t('.microposts', microposts_count: @user.microposts.count) %></h3>
      <ol class="microposts">
        <%= render @microposts %>
      </ol>
      <%= will_paginate @microposts %>
    <% end %>
  </div>
</div>

ユーザー一覧ページから
未フォローのユーザーを表示させるとフォローボタンが
フォロー済のユーザーを選ぶとフォロー解除ボタンが表示される!

フォローボタンを動作させる為の2通りの方法

フォローボタンを使えるようにするためには
標準的(今までやってきた通り)な方法としてRelationshipsコントローラを作って制御する方法(14.2.4でやる)と
Ajaxという仕組みを使う方法(14.2.5でやる)がある
けど、その前に14.2.3でフォローしているユーザーとフォロワーを表示するページをそれぞれ作成してHTMLインターフェイスを完成させちゃうんだって!

演習

  1. ブラウザから /users/2 にアクセスし、フォローボタンが表示されていることを確認してみましょう。同様に、/users/5 では [Unfollow] ボタンが表示されているはずです。さて、/users/1 にアクセスすると、どのような結果が表示されるでしょうか?
  2. ブラウザからHomeページとプロフィールページを表示してみて、統計情報が正しく表示されているか確認してみましょう。
  3. Homeページに表示されている統計情報に対してテストを書いてみましょう。ヒント: リスト 13.28で示したテストに追加してみてください。同様にして、プロフィールページにもテストを追加してみましょう。
  1. /users/2も/users/5も書かれている通りに表示される
    /users/1は自分(current_user)なのでボタンは表示されない
  2. される
  3. リスト13.28で示されているのがプロフィールページのテストな気が🤔
    Homeページのテストだと/sample_app/test/integration/site_layout_test.rbと思うのでなんか作ったらいいのかな?
・
・
・
  test "layout links when logged in" do
    # ログインする
    log_in_as(@user)
    # root_pathへgetのリクエスト
    get root_path
    # static_pages/homeが描画される
    assert_template 'static_pages/home'
    # 特定のHTMLタグが存在する タグの種類(a href), リンク先のパス, タグの数
    assert_select "a[href=?]", root_path, count: 2
    assert_select "a[href=?]", help_path
    assert_select "a[href=?]", users_path
    assert_select "a[href=?]", user_path(@user)
    assert_select "a[href=?]", edit_user_path(@user) 
    assert_select "a[href=?]", logout_path
    assert_select "a[href=?]", about_path
    assert_select "a[href=?]", contact_path
    # 特定のHTMLタグが存在する→ strong id="following"
    assert_select 'strong#following'
    # 描写されたページに@user.following.countを文字列にしたものが含まれる
    assert_match @user.following.count.to_s, response.body
    # 特定のHTMLタグが存在する→ strong id="followers"
    assert_select 'strong#followers'
    # 描写されたページに@user.followers.countを文字列にしたものが含まれる
    assert_match @user.followers.count.to_s, response.body
  end
end
・
・
・
    # 特定のHTMLタグが存在する→ h1のタグに含まれるimg.gravatar
    assert_select 'h1>img.gravatar'
    # 特定のHTMLタグが存在する→ strong id="following"
    assert_select 'strong#following'
    # 描写されたページに@user.following.countを文字列にしたものが含まれる
    assert_match @user.following.count.to_s, response.body
    # 特定のHTMLタグが存在する→ strong id="followers"
    assert_select 'strong#followers'
    # 描写されたページに@user.followers.countを文字列にしたものが含まれる
    assert_match @user.followers.count.to_s, response.body
    # 描画されたページに @userのマイクロポストのcountを文字列にしたものが含まれる 
    assert_match @user.microposts.count.to_s, response.body
    # 特定のHTMLタグが存在する→ class = "pagination"を持つdivが1個
    assert_select 'div.pagination', count: 1
    # @user.micropostsのページネーションの1ページ目の配列を1個ずつ取り出してmicropostに代入
    @user.microposts.paginate(page: 1).each do |micropost|
      # micropostにmicropost.contentが含まれる
      assert_match micropost.content, response.body
    end
  end
end

14.2.3 [Following] と [Followers] ページ

フォローしているユーザーを表示するページとフォロワーを表示するページは
プロフィールページとユーザー一覧ページを合わせたような作りになる
どちらもフォローの統計情報などのユーザー情報を表示するサイドバーとユーザーのリストがある
さらに小さいユーザープロフィール画像のリンクを格子状に並べて表示する
モックアップはRoRT本文参照
まずはフォローしているユーザーのリンクとフォロワーのリンクを動くようにする
Twitterに倣ってどちらのページでもユーザーログインを要求する形に

フォロー/フォロワーページの認可をテストする

今回のテストではfollowingとfollowersの名前付きルートを使っている

  test "should redirect following when not logged in" do
    # /users/@userのid/followingへgetのリクエスト
    get following_user_path(@user)
    # login_urlへリダイレクト
    assert_redirected_to login_url
  end

  test "should redirect followers when not logged in" do
    # /users/@userのid/followersへgetのリクエスト
    get followers_user_path(@user)
    # login_urlへリダイレクト
    assert_redirected_to login_url
  end
end

現状はまだRED
→Usersコントローラに2つの新しいアクションを追加する必要がある

これはリスト 14.15で定義した2つのルーティングにもとづいており、これらはそれぞれfollowingおよびfollowersと呼ぶ必要があります。それぞれのアクションでは、タイトルを設定し、ユーザーを検索し、@user.followingまたは@user.followersからデータを取り出し、ページネーションを行なって、ページを出力する必要があります。

class UsersController < ApplicationController
  # 直前にlogged_in_userメソッド(ApplicationControllerにある)を実行 index,edit,update,following,followersアクションにのみ適用
  before_action :logged_in_user, only: [:index, :edit, :update, :destroy,
                                         :following, :followers]
・
・
・
  def following
    @title = t('.following_title')
    # @userにDBから取得したparams[:id]のuserを代入
    @user  = User.find(params[:id])
    # @usersに@user.followingのページネーションを代入
    @users = @user.following.paginate(page: params[:page])
    render 'show_follow'
  end

  def followers
    @title = t('.followers_title')
    # @userにDBから取得したparams[:id]のuserを代入
    @user  = User.find(params[:id])
    # @usersに@user.followersのページネーションを代入
    @users = @user.followers.paginate(page: params[:page])
    render 'show_follow'
  end
  
  # 外部に公開されないメソッド
  private
・
・
・

Railsはアクションに対応するビューを暗黙的に呼び出してくれる
→例)showアクションの最後でshow.html.erbを呼び出す
上記のfollowing・followers両アクションはrenderを明示的に呼び出し、show_followという同じビューを出力している
よって、それぞれのアクションに別々のビューを用意するのではなくshow_followというビューを1つ作ればOK

<!--ページtitleに → Ruby on Rails @title|Tutorial Sample Appが表示される-->
<% provide(:title, @title) %>
<div class="row">
  <aside class="col-md-4">
    <section class="user_info">
      <%= gravatar_for @user %>
      <h1><%= @user.name %></h1>
      <span><%= link_to "view my profile", @user %></span>
      <span><b>Microposts:</b> <%= @user.microposts.count %></span>
    </section>
    <section class="stats">
      <%= render 'shared/stats' %>
      <% if @users.any? %>
        <div class="user_avatars">
          <% @users.each do |user| %>
            <%= link_to gravatar_for(user, size: 30), user %>
          <% end %>
        </div>
      <% end %>
    </section>
  </aside>
  <div class="col-md-8">
    <h3><%= @title %></h3>
    <% if @users.any? %>
      <ul class="users follow">
        <%= render @users %>
      </ul>
      <%= will_paginate %>
    <% end %>
  </div>
</div>

実行されたアクションによってフォローしているユーザー・フォロワーのいづれかの一覧が表示される
また「現在のユーザー」を使っていない為、他のユーザーの一覧ページも正しく表示される

show_followの統合テスト

ここまでのテストはGREEN
ここからshow_followの描写結果の統合テストを書いていく
HTML構造を網羅的にチェックするテストは少しの変更でもREDになり、逆に生産性を落としかねない
→正しい数が表示されているかどうかと、正しいURLが表示されているかどうかの2つの基本的なテストに留める

$ rails generate integration_test following
      invoke  test_unit
      create    test/integration/following_test.rb

統合テストを用意したらテスト用のデータをそろえる
→リレーションシップ用のfixtureにデータを追加

one:
  follower: michael
  followed: lana

two:
  follower: michael
  followed: malory

three:
  follower: lana
  followed: michael

four:
  follower: archer
  followed: michael

fixtureでは、前半の2つでMichaelがLanaとMaloryをフォローし、後半の2つでLanaとArcherがMichaelをフォローしています。

準備が出来たのでテストを書く

require 'test_helper'

class FollowingTest < ActionDispatch::IntegrationTest
  # setupでmichaelを@userに代入ログイン済とする
  def setup
    @user = users(:michael)
    log_in_as(@user)
  end

  test "following page" do
    # /users/@userのid/followingにgetのリクエスト
    get following_user_path(@user)
    # falseである→ @user.followingがempty
    assert_not @user.following.empty?
    # trueである→ @user.followingのcountを文字列にしたものが本文に一致
    assert_match @user.following.count.to_s, response.body
    # @user.followingを順に取り出してuserに代入
    @user.following.each do |user|
      # 特定のHTMLタグが存在する→ a href = "/users/userのid"
      assert_select "a[href=?]", user_path(user)
    end
  end

  test "followers page" do
    # /users/@userのid/followersにgetのリクエスト
    get followers_user_path(@user)
    # falseである→ @user.followersがempty
    assert_not @user.followers.empty?
    # trueである→ @user.followersのcountを文字列にしたものが本文に一致
    assert_match @user.followers.count.to_s, response.body
    # @user.followersを順に取り出してuserに代入
    @user.followers.each do |user|
      # 特定のHTMLタグが存在する→ a href = "/users/userのid"
      assert_select "a[href=?]", user_path(user)
    end
  end
end

演習

  1. ブラウザから /users/1/followers と /users/1/following を開き、それぞれが適切に表示されていることを確認してみましょう。サイドバーにある画像は、リンクとしてうまく機能しているでしょうか?
  2. リスト 14.29のassert_selectに関連するコードをコメントアウトしてみて、テストが正しく red に変わることを確認してみましょう。

動作確認のみにて省略

[Follow] ボタン (基本編)

ビューが整ったので[Follow] / [Unfollow] ([フォローする]/[フォロー解除])ボタンを動作させる
フォローとフォロー解除はそれぞれリレーションシップの作成と削除に対応しているので、まずはRelationshipsコントローラを作成!

$ rails generate controller Relationships

リレーションシップの基本的なアクセス制御とそれに対するテスト

まずはテストから!と言うことで
Relationshipsコントローラのアクションにアクセスする時、ログイン済のユーザーであるかどうかをチェックするテストを書く
ログイン済でなかった場合ログインページにリダイレクトされる→Relationshipのカウントが変わっていないことを確認

require 'test_helper'

class RelationshipsControllerTest < ActionDispatch::IntegrationTest

  test "create should require logged-in user" do
    # ブロックで渡されたものを呼び出す前後でUser.countに違いがない 
    assert_no_difference 'Relationship.count' do
      # relationships_pathにPOSTのリクエスト
      post relationships_path
    end
    # login_urlにリダイレクト
    assert_redirected_to login_url
  end

  test "destroy should require logged-in user" do
    # # ブロックで渡されたものを呼び出す前後でUser.countに違いがない
    assert_no_difference 'Relationship.count' do
      # oneのidのrelationship_pathにdeleteのリクエスト
      delete relationship_path(relationships(:one))
    end
    # login_urlにリダイレクト
    assert_redirected_to login_url
  end
end

このテストをパスするためにRelationshipsコントローラのアクションに対してlogged_in_userフィルターを追加

class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
  end

  def destroy
  end
end

Relationshipsコントローラのそれぞれのアクション

[Follow] / [Unfollow] ボタンを動作させるためには、フォーム (リスト 14.21リスト 14.22) から送信されたパラメータを使って、followed_idに対応するユーザーを見つけてくる必要があります。

その後、見つけてきたユーザーに対して適切にfollow/unfollowメソッドを実行する

class RelationshipsController < ApplicationController
  # 直前にlogged_in_userメソッド(ApplicationController)を実行
  before_action :logged_in_user

  def create
    # userに代入 params[:followed_id]のデータをUserテーブルから取得
    user = User.find(params[:followed_id])
    # current_userでuserをフォローする
    current_user.follow(user)
    # userのプロフィールページにリダイレクト
    redirect_to user
  end

  def destroy
    # userに代入 Relationshipテーブルからfollowedカラムの内容がparams[:id]のデータを取得
    user = Relationship.find(params[:id]).followed
    # current_userでuserをアンフォローする
    current_user.unfollow(user)
    # userのプロフィールページにリダイレクト
    redirect_to user
  end
end

もしログインしていないユーザーが (curlなどのコマンドラインツールなどを使って) これらのアクションに直接アクセスするようなことがあれば、current_userはnilになり、どちらのメソッドでも2行目で例外が発生します。エラーにはなりますが、アプリケーションやデータに影響は生じません。このままでも支障はありませんが、やはりこのような例外には頼らない方がよいので、上ではひと手間かけてセキュリティのためのレイヤーを追加しました。

logged_in_userフィルターはなくても支障はないけど、ちゃんと設定しておこうというお話

演習

  1. ブラウザ上から /users/2 を開き、[Follow] と [Unfollow] を実行してみましょう。うまく機能しているでしょうか?
  2. 先ほどの演習を終えたら、Railsサーバーのログを見てみましょう。フォロー/フォロー解除が実行されると、それぞれどのテンプレートが描画されているでしょうか?
  1. 動作確認のみにて省略
  2. どちらもusers/show.html.erb

14.2.5 [Follow] ボタン (Ajax編)

ユーザーはプロフィールページを最初に表示し、それからユーザーをフォローし、その後すぐ元のページにリダイレクトされるという流れになります。ユーザーをフォローした後、本当にそのページから離れて元のページに戻らないといけないのでしょうか

Ajaxを使ってWebページからサーバーに「非同期」で、ページを移動することなくリクエストを送信する


Railsでも簡単にAjaxを実装することが出来る

Ajaxを使ったフォーム

# これを
form_for

# こうするだけ
form_for ..., remote: true

実際に定義するとこうなる

<!--current_user(ログイン中のユーザー)と結び付けた新しいRelationshipオブジェクトを返すフォーム-->
<!--Ajaxを採用 → form_for・・・,remote: true-->
<%= form_for(current_user.active_relationships.build, remote: true) do |f| %>
  <!--followed_idに@user.idを渡す-->
  <div><%= hidden_field_tag :followed_id, @user.id %></div>
  <%= f.submit t('.follow_btn'), class: "btn btn-primary" %>
<% end %>
<!--current_user(ログイン中のユーザー)と結び付いたリレーションシップからfollowed_idが @user.idのデータを取得-->
<!--deleteメソッドを指定-->
<!--Ajaxを採用 → form_for・・・,remote: true-->
<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id),
             html: { method: :delete },
             remote: true) do |f| %>
  <%= f.submit t('.unfollow_btn'), class: "btn" %>
<% end %>

生成されるTHMLに関しての解説などはRoRT本文参照

Ajaxリクエストに対応するRelationshipsコントローラ

こういったリクエストの種類によって応答を場合分けするときは、respond_toメソッドというメソッドを使います。

# リクエストの種類によってブロック内のいずれか1行が実行される
respond_to do |format|
  format.html { redirect_to user }
  format.js
end

RelationshipsコントローラでAjaxに対応させるためにrespond_toメソッドをそれぞれのアクションに追加する

このとき、ユーザーのローカル変数 (user) をインスタンス変数 (@user) に変更した点に注目してください。

初めはインスタンス変数は必要なかったけど、Ajaxを使ったフォームを実装したことによりビューでインスタンス変数が必要になったため

class RelationshipsController < ApplicationController
  # 直前にlogged_in_userメソッド(ApplicationController)を実行
  before_action :logged_in_user

  def create
    # @userに代入 userテーブルからparams[:followed_id]のデータを取得
    @user = User.find(params[:followed_id])
    # current_userで@userをフォローする
    current_user.follow(@user)
    # リクエストの種類によってブロック内のいずれか1行が実行される
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end

  def destroy
    # @userに代入 Relationshipテーブルからfollowedカラムの内容がparams[:id]のデータを取得
    @user = Relationship.find(params[:id]).followed
    # current_@serで@userをアンフォローする
    current_@user.unfollow(@user)
    # リクエストの種類によってブロック内のいずれか1行が実行される
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end
end

また、今度はブラウザ側でJavaScriptが無効になっていた場合でもうまく動くようにする
(JavaScriptが無効→Ajaxリクエストが送れない)

・
・
・
    # 認証トークンをremoteフォームに埋め込む
    config.action_view.embed_authenticity_token_in_remote_forms = true
  end
end

難しいことはよくわからなかったんだけど
JSを有効にする設定という事?


ユーザーをフォローしたときやフォロー解除したときにプロフィールページを更新するために
JavaScript用の埋め込みRuby (.js.erb) ファイル (create.js.erbやdestroy.js.erbなど) を編集していく

純粋なJavaScriptと異なり、JS-ERbファイルでは組み込みRuby (ERb) が使えます。create.js.erbファイルでは、フォロー用のフォームをunfollowパーシャルで更新し、フォロワーのカウントを更新するのにERbを使っています (もちろんこれは、フォローに成功した場合の動作です)。

<!--(_follow_form.html.erb内の)id="follow_form"にHTMLを挿入→render('users/unfollow')-->
$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>");
<!--(_stats.html.erb内の)id="followers"の中にHTMLを挿入→<%= @user.followers.count %>-->
$("#followers").html('<%= @user.followers.count %>');

フォロー解除した場合はfollowパーシャルで更新

<!--(_follow_form.html.erb内の)id="follow_form"にHTMLを挿入→render('users/follow')-->
$("#follow_form").html("<%= escape_javascript(render('users/follow')) %>");
<!--(_stats.html.erb内の)id="followers"の中にHTMLを挿入→<%= @user.followers.count %>-->
$("#followers").html('<%= @user.followers.count %>');

escape_javascriptメソッドとは何ぞ

JavaScriptファイル内にHTMLを挿入するときに実行結果をエスケープする

エスケープしておかないと文字化けでうまく作動しなくなる

演習

  1. ブラウザから /users/2 にアクセスし、うまく動いているかどうか確認してみましょう。
  2. 先ほどの演習で確認が終わったら、Railsサーバーのログを閲覧し、フォロー/フォロー解除を実行した直後のテンプレートがどうなっているか確認してみましょう。
  1. 動作確認のみにて省略
  2. フォロー→ relationships/create.js.erb
    フォロー解除→ relationships/destroy.js.erb

14.2.6 フォローをテストする

フォローボタンを設置できたので、バグを検知するためにシンプルなテストを書いていく!

ユーザーのフォローに対するテストでは、 /relationshipsに対してPOSTリクエストを送り、フォローされたユーザーが1人増えたことをチェックします。

通常の、コントローラからアクションを呼び出す形式のフォロー/フォロー解除のテストに加え
Ajaxを利用した場合のテストも必要になる!→「xhr :true」オプションを追加するだけ!

require 'test_helper'

class FollowingTest < ActionDispatch::IntegrationTest
  # setupでmichaelを@userに代入、archerを@otherに代入しそれぞれログイン済とする
  def setup
    @user = users(:michael)
    @other = users(:archer)
    log_in_as(@user)
  end
・
・
・  
  test "should follow a user the standard way" do
    # ブロック内の処理の前後で@user.following.countが1増える
    assert_difference '@user.following.count', 1 do
      # relationships_pathにpostのリクエスト(@other をフォローする)
      post relationships_path, params: { followed_id: @other.id }
    end
  end

  test "should follow a user with Ajax" do
    # ブロック内の処理の前後で@user.following.countが1増える
    assert_difference '@user.following.count', 1 do
      # relationships_pathにAjaxでpostのリクエスト(@other をフォローする)
      post relationships_path, xhr: true, params: { followed_id: @other.id }
    end
  end

  test "should unfollow a user the standard way" do
    # @userが@otherをフォロー
    @user.follow(@other)
    # relationshipに代入 →DBの@userのactive_relationshipsからfollowed_id:が@other.idと一致するデータ
    relationship = @user.active_relationships.find_by(followed_id: @other.id)
    # ブロック内の処理の前後で@user.following.countが-1
    assert_difference '@user.following.count', -1 do
      # relationship_pathにdeleteのリクエスト(relationshipを削除する)
      delete relationship_path(relationship)
    end
  end

  test "should unfollow a user with Ajax" do
    # @userが@otherをフォロー
    @user.follow(@other)
    # relationshipに代入 →DBの@userのactive_relationshipsからfollowed_id:が@other.idと一致するデータ
    relationship = @user.active_relationships.find_by(followed_id: @other.id)
    # ブロック内の処理の前後で@user.following.countが-1
    assert_difference '@user.following.count', -1 do
      # relationship_pathAjaxでにdeleteのリクエスト(relationshipを削除する)
      delete relationship_path(relationship), xhr: true
    end
  end
end

演習

  1. リスト 14.36のrespond_toブロック内の各行を順にコメントアウトしていき、テストが正しくエラーを検知できるかどうか確認してみましょう。実際、どのテストケースが落ちたでしょうか?
  2. リスト 14.40のxhr: trueがある行のうち、片方のみを削除するとどういった結果になるでしょうか? このとき発生する問題の原因と、なぜ先ほどの演習で確認したテストがこの問題を検知できたのか考えてみてください。

1.

# createアクションのformat.html { redirect_to @user }をコメントアウトするとココが落ちる
  test "should follow a user the standard way" do
    # ブロック内の処理の前後で@user.following.countが1増える
    assert_difference '@user.following.count', 1 do
      # relationships_pathにpostのリクエスト(@other をフォローする)
      post relationships_path, params: { followed_id: @other.id }
    end
  end

# 続いてcreateアクションのformat.jsをコメントアウトうとするとココも落ちる
  test "should follow a user with Ajax" do
    # ブロック内の処理の前後で@user.following.countが1増える
    assert_difference '@user.following.count', 1 do
      # relationships_pathにAjaxでpostのリクエスト(@other をフォローする)
      post relationships_path, xhr: true, params: { followed_id: @other.id }
    end
  end

# destroyアクションのformat.html { redirect_to @user }をコメントアウトするとココが落ちる
  test "should unfollow a user the standard way" do
    # @userが@otherをフォロー
    @user.follow(@other)
    # relationshipに代入 →DBの@userのactive_relationshipsからfollowed_id:が@other.idと一致するデータ
    relationship = @user.active_relationships.find_by(followed_id: @other.id)
    # ブロック内の処理の前後で@user.following.countが-1
    assert_difference '@user.following.count', -1 do
      # relationship_pathにdeleteのリクエスト(relationshipを削除する)
      delete relationship_path(relationship)
    end
  end

# 続いてdestroyアクションのformat.jsをコメントアウトするとココも落ちる
  test "should unfollow a user with Ajax" do
    # @userが@otherをフォロー
    @user.follow(@other)
    # relationshipに代入 →DBの@userのactive_relationshipsからfollowed_id:が@other.idと一致するデータ
    relationship = @user.active_relationships.find_by(followed_id: @other.id)
    # ブロック内の処理の前後で@user.following.countが-1
    assert_difference '@user.following.count', -1 do
      # relationship_pathAjaxでにdeleteのリクエスト(relationshipを削除する)
      delete relationship_path(relationship), xhr: true
    end
  end
end

それぞれのformat.js行のみをコメントアウトした場合はテストは落ちない
実際の処理が何で行われているかではなくてブロック内の処理の前後で@user.following.countが変化しているかをテストしているからってことだと思う
with Ajaxじゃないのって思うけど「そういうもの」と思っていてよいっぽい
2.

# test "should follow a user with Ajax"の該当行を削除
FAIL["test_should_follow_a_user_with_Ajax", FollowingTest, 1.5874034440003015]
 test_should_follow_a_user_with_Ajax#FollowingTest (1.59s)
        "@user.following.count" didn't change by 1.
        Expected: 3
          Actual: 2
        test/integration/following_test.rb:49:in `block in <class:FollowingTest>'

# test "should unfollow a user with Ajax"の該当行を削除
FAIL["test_should_unfollow_a_user_with_Ajax", FollowingTest, 1.5098085720001109]
 test_should_unfollow_a_user_with_Ajax#FollowingTest (1.51s)
        "@user.following.count" didn't change by -1.
        Expected: 2
          Actual: 3
        test/integration/following_test.rb:73:in `block in <class:FollowingTest>'

@user.following.countが変わらないのでエラーになる
なぜか→ブロック内で何の処理も行っていない為

まとめとか感想

難しかったって言うか
14.2.6の演習が、そもそも日本語の文章の意図がわからなくてわからん🤔となりましたね。
結局難しくて追いきれなかったんだけど
「そういうもの」って事でいいのかな?って不安だったところを
「そういうものって事でいいかと」って言ってもらったので心置きなくそういう事にしました!

らくだ🐫にもできるRailsチュートリアルとは

「ド」が付く素人のらくだ🐫が勉強するRailsチュートリアルの学習記録です。
自分用に記録していますが、お役に立つことがあれば幸いです。

調べたとはいえらくだ🐫なりの解釈や説明が含まれます。間違っている部分もあるかと思います。そんな所は教えて頂けますと幸いなのですが、このブログにはコメント機能がありません💧お手数おかけしますがTwitterなどでご連絡いただければ幸いです