📋

Rails環境構築後の流れ(自分用)

2021/03/30に公開

1. アプリケーション作成

1-1. 新規Railsプロジェクトの作成 (Rails6)

アプリ作成

rails _6.0.3.4_ new railsns
cd railsns
rails s

デフォルトページ確認

ブラウザで「http://localhost:3000/ 」にアクセスします。


2. トップページの作成

2-1. 画像を配置

app/javascript/images に画像を配置

その後、config/webpacker.yml で images を指定

# 中略

  webpack_compile_output: true

  # Additional paths webpack should lookup modules
  # ['app/assets', 'engine/foo/app/assets']
  resolved_paths: []
  resolved_paths: ['app/javascript/images', 'app/assets/images']

...

2-2. トップページ準備

routes を編集

Rails.application.routes.draw do
  # ここにpagesコントローラーのhomeアクションのルーティングを追加する
  get 'pages/home'
end
$ rails g controller pages

app/controlllers/pages_controller.rb

app/controlllers/pages_controller.rb
class PagesController < ApplicationController

  # ==========ここから追加する==========
  def home
  end
  # ==========ここまで追加する==========

end
from Home
Rails.application.routes.draw do
  # この行を編集する
  root 'pages#home'
end

2-3. webpacker 導入

Bootstrapでは、いくつかのコンポーネントで jQuery, Popper.js といった JavaScript のプラグインが必要なため一緒にインストールします。

yarn add jquery bootstrap popper.js

インストール成功時、package.json というファイルに以下のようなコードが追加されます。
追加されたコードには導入したパッケージとそのパッケージのバージョンが記載されています。

{
  "name": "kanban",
  "private": true,
  "dependencies": {
    "@rails/actioncable": "^6.0.0",
    "@rails/activestorage": "^6.0.0",
    "@rails/ujs": "^6.0.0",
    "@rails/webpacker": "4.3.0",
    "bootstrap": "^4.5.3", // 追加
    "jquery": "^3.5.1", // 追加
    "popper.js": "^1.16.1", // 追加
    "turbolinks": "^5.2.0"
  },
  "version": "0.1.0",
  "devDependencies": {
    "webpack-dev-server": "^3.11.0"
  }
}

導入したパッケージの設定のコードを追加します。config/webpack/environment.js に以下のコードを追加してください。

const { environment } = require('@rails/webpacker')

module.exports = environment

// 以下を追加
  const webpack = require('webpack')
  environment.plugins.append(
    'Provide',
    new webpack.ProvidePlugin({
      $: 'jquery/src/jquery',
      jQuery: 'jquery/src/jquery',
      Popper: ['popper.js', 'default']
    })
  )

app/javascript/stylesheets/application.scssにBootstrapのstyleをimportする記述を追記します。

@import '~bootstrap/scss/bootstrap';

yarn でインストールしたBootstrapのパッケージを利用できるようにimportします。

import 'bootstrap';
import '../stylesheets/application';

2-4. ナビゲーションヘッダーの作成

ナビゲーションヘッダーのビューを作成

<!DOCTYPE html>
<html>
  <head>
    <title>Techpitgram4</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
    <%= stylesheet_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>

    <%# ==========ここから追加する========== %>
    <nav class="navbar navbar-expand-lg navbar-light">
      <div class="container">
        <%= link_to "", root_path, class: "navbar__brand navbar__mainLogo" %>
        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
          <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarSupportedContent">
          <ul class="navbar-nav ml-md-auto align-items-center">
            <li>
              <%= link_to "投稿", "#", class: "btn btn-primary" %>
            </li>
            <li>
              <%= link_to "", "#", class: "nav-link commonNavIcon profile-icon" %>
            </li>
          </ul>
        </div>
      </div>
    </nav>
    <%# ==========ここまで追加する========== %>

    <%= yield %>
  </body>
</html>

ナビゲーションヘッダーのscssファイルをインポート

ナビゲーションヘッダーの見た目を整えるためにナビゲーションヘッダーのscssファイルをインポートします。

@import '~bootstrap/scss/bootstrap';

/* ここの行を追加する */
@import "layouts/navbar";

ナビゲーションヘッダーのscssを追加

app/javascript/stylesheets/ディレクトリの中にlayoutsフォルダを作成し、
作成したlayoutsフォルダの中にnavbar.scssファイルを作成します。
作成したらnavbar.scssに以下のコードを追加してください。

.navbar {
  background-color: #fff;
  border-bottom: 1px solid rgba(0, 0, 0, .0975);
  height: 77px;
  &__brand {
    height: 35px;
    width: 176px;
    background-size: 114px;
  }
  &__mainLogo {
    background-repeat: no-repeat;
    background-image: url("~logo.png");
  }
  &-nav {
    & > li {
      margin-left: 20px;
      &:first-child {
        margin-left: 0;
      }
      .commonNavIcon {
        height: 24px;
        width: 24px;
        background-size: 22px !important;
        background-repeat: no-repeat;
      }
      .profile-icon {
        background-image: url("~parts3.png");
      }
    }
  }
}

部分テンプレートを使って共通化

ビューで共通パーツや繰り返し処理をするパーツは部分テンプレート(複数箇所で使う一部分をテンプレート化してまとめる)を使います。
Navbarはトップページ以外でも使うので部分テンプレートにします。まずapp/views/の中にpartialというディレクトリを作成してください。
作成したpartialというディレクトリに今度は_navbar.html.erbというファイルを作成します。
部分テンプレートのファイル名は先頭にアンダーバーをつけます。
_navbar.html.erbファイルには以下のコードを追加してください。

<nav class="navbar navbar-expand-lg navbar-light">
  <div class="container">
    <%= link_to "", root_path, class: "navbar__brand navbar__mainLogo" %>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarSupportedContent">
      <ul class="navbar-nav ml-md-auto align-items-center">
        <li>
          <%= link_to "投稿", "#", class: "btn btn-primary" %>
        </li>
        <li>
          <%= link_to "", "#", class: "nav-link commonNavIcon profile-icon" %>
        </li>
      </ul>
    </div>
  </div>
</nav>

編集

<!DOCTYPE html>
<html>
  <head>
    <title>Techpitgram6</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <%# <nav>...</nav>の間のコードを消して下の一行に入れ替える %>
    <%= render 'partial/navbar' %>

    <%= yield %>
  </body>
</html>

3. ユーザー関連機能

3-1. Devise インストール

Gemfile に入っているので build しておけば大丈夫
その後、以下 cmd

$ rails generate devise:install

Devise フラッシュメッセージ追加

<!DOCTYPE html>
<html>
  <head>
    <title>Techpitgram4</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
    <%= stylesheet_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <%= render 'partial/navbar' %>

    <%# ==========ここから追加する========== %>
    <% if flash[:notice] %>
      <div class="alert alert-info">
        <%= flash[:notice] %>
      </div>
    <% end %>
    <% if flash[:alert] %>
      <div class="alert alert-danger">
        <%= flash[:alert] %>
      </div>
    <% end %>
    <%# ==========ここまで追加する========== %>

    <%= yield %>
  </body>
</html>

Devise ビューファイルをインストール

$ rails g devise:views

Userモデル作成

$ rails g devise User

Userテーブル作成

$ rails db:migrate

確認

ブラウザ
http://localhost:3000/users/sign_up

ナビゲーションヘッダー非表示

ログインしていないユーザーにはNavbarを表示しないようにしていきます。

<!DOCTYPE html>
<html>
  <head>
    <title>Techpitgram6</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
    <%= stylesheet_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>

    <%# この行にif式を追加する %>
    <%= render 'partial/navbar' if current_user %>

    <% if flash[:notice] %>
      <div class="alert alert-info">
        <%= flash[:notice] %>
      </div>
    <% end %>
    <% if flash[:alert] %>
      <div class="alert alert-danger">
        <%= flash[:alert] %>
      </div>
    <% end %>

    <%= yield %>
  </body>
</html>

サインアウトリンクの追加

link_toでパスを指定する場合、名前付きヘルパーに当たるdestroy_user_sessionに_pathを追加することで、リンクのパスを指定できます。

<% if user_signed_in? %> <%# この行を追加する %>
  <%= link_to "サインアウト", destroy_user_session_path, method: :delete %>
<% end %> <%# この行を追加する %>

<p>このページは仮のトップページです。</p>
<%= link_to "仮のボタンです", "#", class: "btn btn-primary" %>

上記のコードは、もしユーザーがサインインしていれば、サインアウトリンクを表示させます。

users テーブルに name カラムを追加

マイグレーション準備

$ rails g migration AddNameToUser name:string
class AddNameToUser < ActiveRecord::Migration[6.0]
  def change
    # この行を編集する
    add_column :users, :name, :string, null: false
  end
end
$ rails db:migrate

バリデーション

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  # この1行を追加
  validates :name, presence: true, length: { maximum: 50 }
end

nameカラムを保存できるようにする

Deviseを使っている場合、デフォルトだとメールアドレスとパスワードだけパラメータを受け取るようにストロングパラメーターが設定されています。

ストロングパラメーターとは、指定していないパラメーターを受け取るのを禁止します。外部に公開する必要のない情報を誤って公開してしまう可能性が生じるため、そのような事態を防ぐために行います。

class ApplicationController < ActionController::Base

  # ==========ここから追加する==========
  protect_from_forgery with: :exception

  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

    def configure_permitted_parameters
      devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
      devise_parameter_sanitizer.permit(:account_update, keys: [:name])
    end
  # ==========ここまで追加する==========

end

protect_from_forgery with: :exceptionというコードはクロスサイトリクエストフォージェリ (CSRF)への対応策のコードです

サインアップ画面の見た目を変更

ビュー変更

<div class="main">
  <div class="card devise-card">
    <div class="form-wrap">
      <div class="form-group text-center">
        <h2 class="logo-img mx-auto"></h2>
        <p class="text-secondary">友達の写真や動画をチェックしよう</p>
      </div>
      <%= form_with scope: resource, as: resource_name, url: registration_path(resource_name), local: true do |f| %>
        <div class="form-group">
          <%= f.email_field :email, autofocus: true, placeholder: "メールアドレス", class: "form-control" %>
        </div>

        <div class="form-group">
          <%= f.text_field :name, autofocus: true, placeholder: "フルネーム", class: "form-control" %>
        </div>

        <div class="form-group">
          <%= f.password_field :password, autocomplete: "off", placeholder: "パスワード", class: "form-control" %>
        </div>

        <div class="form-group">
          <%= f.password_field :password_confirmation, autocomplete: "off", placeholder: "パスワードの確認", class: "form-control" %>
        </div>

        <div class="actions">
          <%= f.submit "登録する", class: "btn btn-primary w-100" %>
        </div>
      <% end %>

      <br>

      <p class="devise-link">
        アカウントをお持ちですか?
        <%= link_to "サインインする", new_user_session_path %>
      </p>
    </div>
  </div>
</div>

scss import

@import '~bootstrap/scss/bootstrap';

@import "layouts/navbar";

/* ===ここから追加する=== */
@import "common";
@import "users/devise";
/* ===ここまで追加する=== */

add scss for signup

body {
  background-color: #fafafa;
  font-size: 14px;
  color: #262626;
}
.main {
  max-width: 960px;
  margin: 0 auto;
  width: 90%;
  padding: 100px 75px 100px;
  display: block;
  flex: 1;
  flex-basis: auto;
  box-sizing: border-box;
}

.devise-card {
  border: none;
}

.form-wrap{
  max-width: 400px;
  width: 100%;
  margin: 50px auto;
}

.logo-img {
  background-image: url("~logo.png");
  background-repeat: no-repeat;
  height: 56px;
  width: 180px;
  background-size: 160px;
  background-size: 180px!important;
}

.form-control {
  border: 1px solid #dcdfe6;
  font-size: inherit;
}

.devise-link {
  text-align: center;
  margin: 16px 0;
}

# wrapper {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

footer {
  margin-top: auto;
}

@media screen and (max-width: 768px) {
  .main {
    padding: 75px 0;
  }
  .devise-card {
    background-color: #fafafa;
  }
}

footer作成

new

<!DOCTYPE html>
<html>
  <head>
    <title>Techpitgram</title>
    <%= csrf_meta_tags %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
    <%= stylesheet_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <%# ========== ここから編集する ========== %>
    <div id="wrapper">
       <%= render 'partial/navbar' if current_user %>

       <% if flash[:notice] %>
         <div class="alert alert-info"><%= flash[:notice] %></div>
       <% end %>
       <% if flash[:alert] %>
         <div class="alert alert-danger"><%= flash[:alert] %></div>
       <% end %>
       <%= yield %>

       <%# ========== partial/footerを追加する ========== %>
       <%= render 'partial/footer' %>

    </div>
    <%# ========== ここまで編集する ========== %>

  </body>
</html>
<footer>
  <div class="container">
    <div class="text-center">
      <p>
        Copyright © Techpit
      </p>
    </div>
  </div>
</footer>

サインイン画面の修正

ビュー作成

<div class="main">
  <div class="card devise-card">
    <div class="form-wrap">
      <div class="form-group text-center">
        <h2 class="logo-img mx-auto"></h2>
      </div>
      <%= form_with scope: resource, as: resource_name, url: session_path(resource_name), local: true do |f| %>
        <%= devise_error_messages! %>
        <div class="form-group">
          <%= f.email_field :email, autofocus: true, placeholder: "メールアドレス", class: "form-control" %>
        </div>

        <div class="form-group">
          <%= f.password_field :password, autocomplete: "off", placeholder: "パスワード", class: "form-control" %>
        </div>

        <div class="actions">
          <%= f.submit "サインインする", class: "btn btn-primary w-100" %>
        </div>
      <% end %>

      <br>

      <p class="devise-link">
        アカウントをお持ちでないですか?
        <%= link_to "登録する", new_user_registration_path %>
      </p>

    </div>
  </div>
</div>

http://localhost:3000/users/sign_in にアクセスして確認

4. ユーザープロフィール機能

プロフィールページの作成

routing

usersコントローラーのshowアクションのルーティングをプロフィールページのルーティングとして設定していきます。

Rails.application.routes.draw do
  devise_for :users
  root 'pages#home'

  # ここにusersコントローラーのshowアクションのルーティングを追加する
  get '/users/:id', to: 'users#show', as: 'user'
end

asオプションを使うと、ルーティングに名前をつけることができます。
今回userという名前をつけたので、user_pathというメソッドが生成され、コントローラー・ヘルパー・ビューで使えるようになります。

controller

$ rails g controller users
class UsersController < ApplicationController

  # ==========ここから追加する==========
  def show
    @user = User.find_by(id: params[:id])
  end
  # ==========ここまで追加する==========

end

profile_photo カラムを追加

$ rails g migration AddProfilePhotoToUsers profile_photo:string
$ rails db:migrate

profile_photoカラムに何もない場合、デフォルトのアイコンを表示

アイコンはユーザーの詳細ページや投稿一覧ページでも表示する予定なので、どのビューでも呼べるようにapp/helpers/application_helper.rbにメソッドを用意しておきましょう。


  # ==========ここから追加する==========
  def avatar_url(user)
    return user.profile_photo unless user.profile_photo.nil?
    gravatar_id = Digest::MD5::hexdigest(user.email).downcase
    "https://techpit-market-prod.s3.amazonaws.com/uploads/part_attachment/file/15782/2da91636-af73-4eed-91cd-320a0399609c.jpg"
  end
  # ==========ここまで追加する==========

end

プロフィールページのビューを作成

<!DOCTYPE html>
<html>
  <head>
    <title>Techpitgram</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%# ここの1行も追加 %>
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
    <%= stylesheet_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <div id="wrapper">
      <%= render 'partial/navbar' if current_user %>

      <% if flash[:notice] %>
        <div class="alert alert-info">
          <%= flash[:notice] %>
        </div>
      <% end %>
      <% if flash[:alert] %>
        <div class="alert alert-danger">
          <%= flash[:alert] %>
        </div>
      <% end %>

      <%# ==========ここにcontainerクラスを追加する========== %>
      <div class="container">
        <%= yield %>
      </div>
      <%# ==========divの閉じタグを追加========== %>

      <%= render 'partial/footer' %>
    </div>
  </body>
</html>

プロフィールページのビューファイルを作成します。今回、usersコントローラーのshowアクションを定義したので、app/views/users/の中にshow.html.erbというファイルを作成します。

<div class="profile-wrap">
  <div class="row">
    <div class="col-md-4 text-center">
      <%= image_tag avatar_url(@user), class: "round-img" %>
    </div>
    <div class="col-md-8">
      <div class="row">
        <h1><%= @user.name %></h1>
        <%= link_to "プロフィールを編集", edit_user_registration_path, class: "btn btn-outline-dark common-btn edit-profile-btn" %>
        <button type="button" class="setting" data-toggle="modal" data-target="#exampleModal"></button>

        <div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
          <div class="modal-dialog" role="document">
            <div class="modal-content">
              <div class="modal-header">
                <h5 class="modal-title" id="exampleModalLabel">設定</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                  <span aria-hidden="true">×</span>
                </button>
              </div>
              <div class="list-group text-center">
                <%= link_to "サインアウト", destroy_user_session_path, method: :delete, class: "list-group-item list-group-item-action" %>
                <%= link_to "キャンセル", "#", class: "list-group-item list-group-item-action", "data-dismiss": "modal" %>
              </div>
            </div>
          </div>
        </div>
      </div>
      <% if @user == current_user %>
        <div class="row">
          <p>
            <%= @user.email %>
          </p>
        </div>
      <% end %>
    </div>
  </div>
</div>

次にプロフィールページのscssを追加します。

@import '~bootstrap/scss/bootstrap';

@import "layouts/navbar";

@import "common";

@import "users/devise";

/* ここの行を追加する */
@import "users/show";

new!

.profile-wrap{
  margin:40px 0px;
}

.round-img{
  border-radius:50%;
  width: 120px;
  height: 120px;
}

.edit-profile-btn {
  margin: 15px 0 0 15px;
  font-weight: bold;
  height: 26px;
  line-height: 26px;
  padding: 0 26px;
  border-color: #dbdbdb;
  font-size: 14px;
}

.setting {
  background-image: image-url("~parts4.png");
  background-repeat: no-repeat;
  height: 24px;
  width: 24px;
  background-color: transparent;
  margin: 18px 0 0 10px;
  background-size: 22px!important;
  border: none;
  &:hover {
    cursor: pointer;
  }
}

1回リセット

$ rails db:reset

ナビゲーションヘッダーにプロフィールページに飛ぶリンクを追加

<nav class="navbar navbar-expand-lg navbar-light">
  <div class="container">
    <%= link_to "", root_path, class: "navbar__brand navbar__mainLogo" %>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarSupportedContent">
      <ul class="navbar-nav ml-md-auto align-items-center">
        <li>
          <%= link_to "投稿", "#", class: "btn btn-primary" %>
        </li>
        <li>

          <%# ==========この行を編集する========== %>
          <%= link_to "", user_path(current_user), class: "nav-link commonNavIcon profile-icon" %>

        </li>
      </ul>
    </div>
  </div>
</nav>
$ rails db:reset

プロフィール編集機能

表示を限定

<div class="profile-wrap">
  <div class="row">
    <div class="col-md-4 text-center">
      <%= image_tag avatar_url(@user), class: "round-img" %>
    </div>
    <div class="col-md-8">
      <div class="row">
        <h1><%= @user.name %></h1>

        <%# ===この行を追加する=== %>
        <% if @user == current_user %>

          <%= link_to "プロフィールを編集", edit_user_registration_path, class: "btn btn-outline-dark edit-profile-btn" %>
          <button type="button" class="setting" data-toggle="modal" data-target="#exampleModal"></button>

        <%# ===この行を追加する=== %>
        <% end %>


        <div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
          <div class="modal-dialog" role="document">
            <div class="modal-content">
              <div class="modal-header">
                <h5 class="modal-title" id="exampleModalLabel">設定</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                  <span aria-hidden="true">×</span>
                </button>
              </div>
              <div class="list-group text-center">
                <%= link_to "ログアウト", destroy_user_session_path, method: :delete, class: "list-group-item list-group-item-action" %>
                <%= link_to "キャンセル", "#", class: "list-group-item list-group-item-action", "data-dismiss": "modal" %>
              </div>
            </div>
          </div>
        </div>
      </div>
      <div class="row">
        <p>
          <%= @user.email %>
        </p>
      </div>
    </div>
  </div>
</div>

add controller

Deviseのデフォルトの状態だと、ユーザーのアカウントをアップデートするには、パスワード・確認用パスワード・カレントパスワードの3つ必要になります。
ただプロフィールを編集するのに3つのパスワードを入力させるのは、ユーザーとしては非常に手間です。
なので、パスワードを入力しなくてもプロフィールの情報を編集できるように実装していきます。
https://easyramble.com/user-account-update-without-password-on-devise.html

config/routes.rb
Rails.application.routes.draw do
  devise_for :users, # ここの行にカンマを追加
    controllers: { registrations: 'registrations' } # ここの行を追加

  root 'pages#home'

  get '/users/:id', to: 'users#show', as: 'user'
end
class RegistrationsController < Devise::RegistrationsController

  protected

  def update_resource(resource, params)
    resource.update_without_current_password(params)
  end
end

次にUserモデルにupdate_without_current_passwordメソッドを実装します。

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  validates :name, presence: true, length: { maximum: 50 }

  # ==========ここから追加する==========
  def update_without_current_password(params, *options)
    params.delete(:current_password)

    if params[:password].blank? && params[:password_confirmation].blank?
      params.delete(:password)
      params.delete(:password_confirmation)
    end

    result = update_attributes(params, *options)
    clean_up_passwords
    result
  end
  # ==========ここまで追加する==========

end

プロフィール編集ページの design

プロフィール編集ページに関しては、サインアップ画面やサインイン画面同様、Deviseが提供しているビューファイルを使います。
(Deviseを導入するときに、rails g devise:viewsというコマンドを実行しました。これらのビューファイルはこのコマンドを実行したときに追加されたファイルです。)

<div class="col-md-offset-2 mb-4 edit-profile-wrapper">
  <div class="row">
    <div class="col-md-8 mx-auto">
      <div class="profile-form-wrap">
        <%= form_with scope: resource, as: resource_name, url: registration_path(resource_name), local: true, method: :patch do |f| %>
          <div class="form-group">
            <%= f.label :name, "名前" %>
            <%= f.text_field :name, autofocus: true, class: "form-control" %>
          </div>

          <div class="form-group">
            <%= f.label :email, "メールアドレス" %>
            <%= f.email_field :email, autofocus: true, class: "form-control" %>
          </div>

          <div class="form-group">
            <%= f.label :password, "パスワード" %>
            <%= f.password_field :password, autofocus: "off", class: "form-control" %>
          </div>

          <div class="form-group">
            <%= f.label :password_confirmation, "パスワードの確認" %>
            <%= f.password_field :password_confirmation, autofocus: "off", class: "form-control" %>
          </div>

          <%= f.submit "変更する", class: "btn btn-primary" %>
        <% end %>
      </div>
    </div>
  </div>
</div>

プロフィール編集ページのscssを追加します。

まずプロフィールページのscssファイルをインポートするための記述をapplication.scssに追記します。

@import '~bootstrap/scss/bootstrap';

@import "layouts/navbar";

@import "common";

@import "users/devise";
@import "users/show";

/* ここの行を追加する */
@import "users/edit";
.profile-form-wrap {
  background: #fff;
  padding: 20px;
  border: 1px solid #e6e6e6;
}

.profile-form-wrap label {
  font-weight: bold;
}

.edit-profile-wrapper {
  margin-top: 60px;
}

http://localhost:3000/users/edit にアクセス

プロフィール編集後のリダイレクト先を変更

現状でもプロフィールは編集できるのですが、Deviseのデフォルトだとプロフィール編集後、トップページに遷移します。

class RegistrationsController < Devise::RegistrationsController

  protected

  def update_resource(resource, params)
    resource.update_without_current_password(params)
  end

  # ==========ここから追加する==========
  def after_update_path_for(resource)
    user_path(resource)
  end
  # ==========ここまで追加する==========

end

after_update_path_forメソッドはDeviseが用意しているメソッドです。
アカウントをアップデートさせた後、どのパスに遷移させるかを指定できます。
なので、プロフィール編集後はログインしているユーザーのプロフィールページにリダイレクトするように実装していきます。

5. 投稿機能

モデル作成

eferences型で保存すると、user_idを外部キーとして明示的に指定できます。
外部キーとは関連したテーブルの間を結ぶために設定する列のことです。(今回でいうとpostsテーブルとusersテーブル。)
モデルを作成するには、rails g model モデル名というコマンドでモデルを作成できます。

$ rails g model Post
class CreatePosts < ActiveRecord::Migration[6.0]
  def change
    create_table :posts do |t|
      # ==========ここから追加する==========
      t.string :caption
      t.references :user, foreign_key: true, null: false
      # ==========ここまで追加する==========
      t.timestamps
    end
  end
end

references型は参照先テーブル名の単数形+"_id"がカラム名として設定されるので、今回postsテーブルにはuser_idというカラムが追加されます。
foreign_key: trueは外部キーとして使用するということを示しています。

$ rails db:migrate

Photo モデル

$ rails g model Photo
class CreatePhotos < ActiveRecord::Migration[6.0]
  def change
    create_table :photos do |t|
      # ==========ここから追加する==========
      t.string :image, null: false
      t.references :post, foreign_key: true, null: false
      # ==========ここまで追加する==========
      t.timestamps
    end
  end
end
$ rails db:migrate

アソシエーション

アソシエーションとは、2つのモデル同士のつながりを指します。モデルとモデルの間には関連付けを行う必要があります。

UserモデルとPostモデルのアソシエーションの設定

まずUserモデルとPostモデルのアソシエーションを設定していきます。

上記2つのモデルの関係性は以下のようになります。

  • ユーザーは複数の投稿することができる
  • 投稿Aに関して、投稿Aを投稿したユーザーは一人しかいない
class User < ApplicationRecord

  # この行を追加する
  has_many :posts, dependent: :destroy

  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  .
  .
  .

has_manyは、他のモデルとの間に「1対多」のつながりがあることを示します。(「1側」にhas_manyを追加します。)
has_manyが使用されている場合、「反対側」のモデルでは多くの場合belongs_toが使われます。

またdependent: :destroyをつけることで、オブジェクトが削除されるときに、
関連付けられたオブジェクトのdestroyメソッドが実行されます。
つまり今回で言うと、ユーザーが削除されたら、そのユーザーに紐づく投稿も削除します。

class Post < ApplicationRecord

  # ここに追加する
  belongs_to :user

end

PostモデルとPhotoモデルのアソシエーション設定

同じ要領で今度はPostモデルとPhotoモデルのアソシエーションを設定します。

PostモデルとPhotoモデルは「1対多」の関係になります。

class Post < ApplicationRecord
  belongs_to :user

  # ここに追加
  has_many :photos, dependent: :destroy
end
class Photo < ApplicationRecord

  # ここに追加する
  belongs_to :post

end

バリデーション

class Photo < ApplicationRecord
  belongs_to :post

  # この行を追加する
  validates :image, presence: true
end

carrierwave と MiniMagick の導入

新たな準備は不要である。
以下の部分で準備は整っている。

gem 'carrierwave', '~> 2.0'

CarrierWaveのアップローダーを作成

$ rails g uploader image

モデルのカラムにアップローダーを紐付け

Photoモデルのimageカラムと、先ほど作成したアップローダーImageUploaderと紐付けをします。
(ImageUploaderはapp/uploaders/image_uploader.rbのクラスの名前です。)

class Photo < ApplicationRecord
  belongs_to :post

  validates :image, presence: true

  # ここを追加
  mount_uploader :image, ImageUploader
end

MiniMagick

RUN apk update && \
    apk upgrade && \
    apk add --no-cache \
...
        imagemagick \
gem "mini_magick"

アップローダーの記述を修正

class ImageUploader < CarrierWave::Uploader::Base
  # Include RMagick or MiniMagick support:
  # include CarrierWave::RMagick
  include CarrierWave::MiniMagick # ここのコメントアウトを外す

  # Choose what kind of storage to use for this uploader:
  storage :file
  # storage :fog

  # Override the directory where uploaded files will be stored.
  # This is a sensible default for uploaders that are meant to be mounted:
  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  # Provide a default URL as a default if there hasn't been a file uploaded:
  # def default_url(*args)
  #   # For Rails 3.1+ asset pipeline compatibility:
  #   # ActionController::Base.helpers.asset_path("fallback/" + [version_name, "default.png"].compact.join('_'))
  #
  #   "/images/fallback/" + [version_name, "default.png"].compact.join('_')
  # end

  # Process files as they are uploaded:
  # process scale: [200, 300]
  #
  # def scale(width, height)
  #   # do something
  # end

  # Create different versions of your uploaded files:
  # version :thumb do
  #   process resize_to_fit: [50, 50]
  # end

  # ==========ここから追加する==========
  version :medium do
    process resize_to_fill: [1080, 1080]
  end
  # ==========ここまで追加する==========

  # Add a white list of extensions which are allowed to be uploaded.
  # For images you might use something like this:

  # ここのコメントアウトを外す
  def extension_whitelist
    %w(jpg jpeg gif png)
  end

  # Override the filename of the uploaded files:
  # Avoid using model.id or version_name here, see uploader/store.rb for details.
  # def filename
  #   "something.jpg" if original_filename
  # end
end

mediumというバージョンが作成され、画像を1080 x 1080ピクセルにリサイズします。

投稿機能

ルーティングの追加

Rails.application.routes.draw do
  devise_for :users,
    controllers: { registrations: 'registrations' }

  root 'pages#home'

  get '/users/:id', to: 'users#show', as: 'user'

  # ==========ここから編集する==========
  resources :posts, only: %i(new create) do
    resources :photos, only: %i(create)
  end
  # ==========ここまで編集する==========

end

コントローラーの作成

$ rails g controller posts

作成したコントローラーにアクションを追加

class PostsController < ApplicationController
  def new
    @post = Post.new
    @post.photos.build
  end

  # ==========ここから追加する==========
  def create
    @post = Post.new(post_params)
    if @post.photos.present?
      @post.save
      redirect_to root_path
      flash[:notice] = "投稿が保存されました"
    else
      redirect_to root_path
      flash[:alert] = "投稿に失敗しました"
    end
  end

  private
    def post_params
      params.require(:post).permit(:caption, photos_attributes: [:image]).merge(user_id: current_user.id)
    end
  # ==========ここまで追加する==========

end

インスタンス変数@postはビューでフォームを作成する際に使います。

このコードはnewアクションと同じようにnewメソッドを使って、インスタンスを作成しています。
newアクションのときと違うのはpost_paramsというメソッドを引数で呼び出しているところです。
なので次はpost_paramsメソッドがあるコードを見てみましょう。

レシーバとは、あるオブジェクトに対してメソッドを実行しようとした場合、そのメソッドの働きかけるオブジェクトのことです。

paramsとは送られてきたリクエスト情報をひとまとめにしたものです。
requireで受け取る値のキーを設定します。
permitで変更を加えられるキーを指定します。今回の場合、captionキーとimageキーを指定しています。
mergeメソッドは2つのハッシュを統合するメソッドです。今回は誰が投稿したかという情報が必要なためuser_idの情報を統合しています。

また、accepts_nested_attributes_forは、
親子関係のある関連モデル(今回でいうとPostモデルとPhotoモデル)で、親から子を作成したり保存するときに使えます。

今回投稿する際にPostモデルの子に値するPhotoモデルを通して、photosテーブルに写真を保存します。
accepts_nested_attributes_forメソッドを親のモデル(Postモデル)に追加する必要があります。

class Post < ApplicationRecord
  belongs_to :user
  has_many :photos, dependent: :destroy

  # ここに追加
  accepts_nested_attributes_for :photos
end

ビューを作成

<%= form_with model: @post do |f| %>
  <%= f.label :caption %>
  <%= f.text_field :caption %>
  <%= f.fields_for :photos do |i| %>
    <%= i.file_field :image %>
  <% end %>
  <%= f.submit "投稿", class: "btn btn-primary" %>
<% end %>

http://localhost:3000/posts/new 」にアクセス

サインイン済みユーザーのみにアクセス許可

class PostsController < ApplicationController
  # ここに追加する
  before_action :authenticate_user!

  def new
  .
  .
  .

Gitでコードを管理している場合、今のままだと、画像をアップロードする度に画像がコミット対象になります。
なので.gitignoreというファイルに以下のコードを追加することでアップロードした画像はコミットしないようにします。

投稿一覧

ルーティングの設定

Rails.application.routes.draw do
  devise_for :users,
    controllers: { registrations: 'registrations' }

  root 'pages#home'

  get '/users/:id', to: 'users#show', as: 'user'

  # この行を編集する
  resources :posts, only: %i(new create index) do
    resources :photos, only: %i(create)
  end
end

コントローラーの編集

class PostsController < ApplicationController
  def new
    @post = Post.new
    @post.photos.build
  end

  def create
    @post = Post.new(post_params)
    if @post.photos.present?
      @post.save
      redirect_to root_path
      flash[:notice] = "投稿が保存されました"
    else
      redirect_to root_path
      flash[:alert] = "投稿に失敗しました"
    end
  end

  # ==========ここから追加する==========
  def index
    @posts = Post.limit(10).includes(:photos, :user).order('created_at DESC')

  end
  # ==========ここまで追加する==========

  private
    def post_params
      params.require(:post).permit(:caption, photos_attributes: [:image]).merge(user_id: current_user.id)
    end
end

ビューを作成

<% @posts.each do |post| %>
  <div class="col-md-8 col-md-2 mx-auto">
    <div class="card-wrap">
      <div class="card">
        <div class="card-header align-items-center d-flex">
          <%= link_to user_path(post.user), class: "no-text-decoration" do %>
            <%= image_tag avatar_url(post.user), class: "post-profile-icon" %>
          <% end %>
          <%= link_to user_path(post.user), class: "black-color no-text-decoration",
            title: post.user.name do %>
            <strong><%= post.user.name %></strong>
          <% end %>
        </div>

        <%= image_tag post.photos.first.image.url(:medium), class: "card-img-top" %>

        <div class="card-body">
          <div class="row parts">
            <%= link_to "", "#", class: "love" %>
            <%= link_to "", "#", class: "comment" %>
          </div>
          <div><strong>「いいね!」10件</strong></div>
          <div>
            <span><strong><%= post.user.name %></strong></span>
            <span><%= post.caption %></span>
            <%= link_to time_ago_in_words(post.created_at).upcase + "前", "#", class: "post-time no-text-decoration" %>
            <hr>
            <div class="row parts">
              <form action="#" class="w-100">
                <div>
                  <textarea class="form-control comment-input border-0" placeholder="コメント..." rows="1"></textarea>
                </div>
              </form>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
<% end %>
@import '~bootstrap/scss/bootstrap';

@import "layouts/navbar";

@import "common";

@import "users/devise";
@import "users/show";
@import "users/edit";

/* ここの行を追加する */
@import "posts";

new!

.post-profile-icon {
  height: 40px;
  width: 40px;
  border-radius: 50%;
  margin-right: 10px;
}

.card-wrap {
  margin: 40px 0px;
}

.no-text-decoration:hover {
  text-decoration: none;
}

.black-color {
  color: #262626;
}

.parts {
  margin: 12px 0;
}

.love {
  background-image: url("~parts5");
  background-repeat: no-repeat;
  height: 36px;
  width: 36px;
  background-size: 36px !important;
}

.comment {
  margin-left: 8px;
  background-image: url("~parts6");
  background-repeat: no-repeat;
  height: 36px;
  width: 36px;
  background-size: 40px !important;
}

.post-time {
  margin:0;
  color:#999;
  font-size:10px;
}

.post-sub-text {
  text-decoration: none;
  color: #262626;
}

デフォルトの言語を日本語に設定します

  .
  .
  .
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 5.2

    # この行を追加する
    config.i18n.default_locale = :ja

    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration can go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded after loading
    # the framework and any gems in your application.
  end
end
ja:
  datetime:
    distance_in_words:
      half_a_minute: "30秒前後"
      less_than_x_seconds:
        one:   "1秒"
        other: "%{count}秒"
      x_seconds:
        one:   "1秒"
        other: "%{count}秒"
      less_than_x_minutes:
        one:   "1分"
        other: "%{count}分"
      x_minutes:
        one:   "約1分"
        other: "%{count}分"
      about_x_hours:
        one:   "約1時間"
        other: "約%{count}時間"
      x_days:
        one:   "1日"
        other: "%{count}日"
      about_x_months:
        one:   "約1ヶ月"
        other: "約%{count}ヶ月"
      x_months:
        one:   "1ヶ月"
        other: "%{count}ヶ月"
      almost_x_years:
        one:   "1年弱"
        other: "%{count}年弱"
      about_x_years:
        one:   "約1年"
        other: "約%{count}年"
      over_x_years:
        one:   "1年以上"
        other: "%{count}年以上"

  devise:
    confirmations:
      confirmed: 'アカウントを登録しました。'
      send_instructions: 'アカウントの有効化について数分以内にメールでご連絡します。'
      send_paranoid_instructions: "あなたのメールアドレスが登録済みの場合、本人確認用のメールが数分以内に送信されます。"
    failure:
      already_authenticated: 'すでにサインインしています。'
      inactive: 'アカウントが有効化されていません。メールに記載された手順にしたがって、アカウントを有効化してください。'
      invalid: "%{authentication_keys} もしくはパスワードが不正です。"
      locked: 'あなたのアカウントは凍結されています。'
      last_attempt: 'あなたのアカウントが凍結される前に、複数回の操作がおこなわれています。'
      not_found_in_database: "%{authentication_keys} もしくはパスワードが不正です。"
      timeout: 'セッションがタイムアウトしました。もう一度サインインしてください。'
      unauthenticated: 'アカウント登録もしくはサインインしてください。'
      unconfirmed: 'メールアドレスの本人確認が必要です。'
    mailer:
      confirmation_instructions:
        subject: 'アカウントの有効化について'
      reset_password_instructions:
        subject: 'パスワードの再設定について'
      unlock_instructions:
        subject: 'アカウントの凍結解除について'
      password_change:
        subject: 'パスワードの変更について'
    omniauth_callbacks:
      failure: "%{kind} アカウントによる認証に失敗しました。理由:(%{reason})"
      success: "%{kind} アカウントによる認証に成功しました。"
    passwords:
      no_token: "このページにはアクセスできません。パスワード再設定メールのリンクからアクセスされた場合には、URL をご確認ください。"
      send_instructions: 'パスワードの再設定について数分以内にメールでご連絡いたします。'
      send_paranoid_instructions: "あなたのメールアドレスが登録済みの場合、パスワード再設定用のメールが数分以内に送信されます。"
      updated: 'パスワードが正しく変更されました。'
      updated_not_active: 'パスワードが正しく変更されました。'
    registrations:
      destroyed: 'アカウントを削除しました。またのご利用をお待ちしております。'
      signed_up: 'アカウント登録が完了しました。'
      signed_up_but_inactive: 'サインインするためには、アカウントを有効化してください。'
      signed_up_but_locked: 'アカウントが凍結されているためサインインできません。'
      signed_up_but_unconfirmed: '本人確認用のメールを送信しました。メール内のリンクからアカウントを有効化させてください。'
      update_needs_confirmation: 'アカウント情報を変更しました。変更されたメールアドレスの本人確認のため、本人確認用メールより確認処理をおこなってください。'
      updated: 'アカウント情報を変更しました。'
    sessions:
      signed_in: 'サインインしました。'
      signed_out: 'サインアウトしました。'
      already_signed_out: '既にサインアウト済みです。'
    unlocks:
      send_instructions: 'アカウントの凍結解除方法を数分以内にメールでご連絡します。'
      send_paranoid_instructions: 'アカウントが見つかった場合、アカウントの凍結解除方法を数分以内にメールでご連絡します。'
      unlocked: 'アカウントを凍結解除しました。'
  errors:
    messages:
      already_confirmed: 'は既に登録済みです。サインインしてください。'
      confirmation_period_expired: "の期限が切れました。%{period} までに確認する必要があります。 新しくリクエストしてください。"
      expired: 'の有効期限が切れました。新しくリクエストしてください。'
      not_found: 'は見つかりませんでした。'
      not_locked: 'は凍結されていません。'
      not_saved:
        one: "エラーが発生したため %{resource} は保存されませんでした:"
        other: "%{count} 件のエラーが発生したため %{resource} は保存さ

rootルーティングの編集

投稿一覧ページを「http://localhost:3000」にアクセスしたら表示できるようにルーティングの設定を行います。

Rails.application.routes.draw do
  devise_for :users,
    controllers: { registrations: 'registrations' }

  # この行を編集する
  root 'posts#index'

  get '/users/:id', to: 'users#show', as: 'user'

  resources :posts, only: %i(index new create) do
    resources :photos, only: %i(create)
  end
end

投稿ページの見た目

投稿ページに遷移するリンクを追加

link_toでパスを指定する場合、名前付きヘルパーに当たるnew_postに_pathを追加することで、リンクのパスを指定できます。

<nav class="navbar navbar-expand-lg navbar-light">
  <div class="container">
    <%= link_to "", root_path, class: "navbar__brand navbar__mainLogo" %>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarSupportedContent">
      <ul class="navbar-nav ml-md-auto align-items-center">
        <li>

          <%# =====この行を編集する===== %>
          <%= link_to "投稿", new_post_path, class: "btn btn-primary" %>

        </li>
        <li>
          <%= link_to "", user_path(current_user), class: "nav-link commonNavIcon profile-icon" %>
        </li>
      </ul>
    </div>
  </div>
</nav>

ビューを編集

<div class="d-flex flex-column align-items-center mt-3">
  <div class="col-xl-7 col-lg-8 col-md-10 col-sm-11 post-card">
    <div class="card">
      <div class="card-header">
        投稿画面
      </div>
      <div class="card-body">
        <%= form_with model: @post, class: "upload-images p-0 border-0" do |f| %>
          <div class="form-group row mt-2">
            <div class="col-auto pr-0">
              <%= image_tag avatar_url(current_user), class: "post-profile-icon" %>
            </div>
            <div class="col pl-0">
              <%= f.text_field :caption, class: "form-control border-0", placeholder: "キャプションを書く" %>
            </div>
          </div>
          <div class="mb-3">

            <%= f.fields_for :photos do |i| %>
              <%= i.file_field :image %>
            <% end %>

          </div>
          <%= f.submit "投稿する", class: "btn btn-primary" %>
        <% end %>
      </div>
    </div>
  </div>
</div>

投稿詳細ページ

ルーティングの設定

config/routes.rb
Rails.application.routes.draw do
devise_for :users,
controllers: { registrations: 'registrations' }

root 'posts#index'

get '/users/:id', to: 'users#show', as: 'user'

この行を編集する

resources :posts, only: %i(index new create show) do
resources :photos, only: %i(create)
end
end

コントローラーの編集

class PostsController < ApplicationController
  before_action :authenticate_user!

  def new
    @post = Post.new
    @post.photos.build
  end

  def create
    @post = Post.new(post_params)
    if @post.photos.present?
      @post.save
      redirect_to root_path
      flash[:notice] = "投稿が保存されました"
    else
      redirect_to root_path
      flash[:alert] = "投稿に失敗しました"
    end
  end

  def index
    @posts = Post.limit(10).includes(:photos, :user).order('created_at DESC')
  end

  # ==========ここから追加する==========
  def show
    @post = Post.find_by(id: params[:id])
  end
  # ==========ここまで追加する==========

  private
    def post_params
      params.require(:post).permit(:caption, photos_attributes: [:image]).merge(user_id: current_user.id)
    end
end

受け取ったHTTPリクエストからidを判別し、指定のレコード1つを@postに代入しています。
@postに投稿の情報を入れることで、@postを使ってビューに投稿のキャプションや写真を表示させることができます。

ビューを作成・編集

<div class="col-md-10 col-md-offset-1 mx-auto postShow-wrap">
  <div class="row post-wrap">
    <div class="col-md-8">
      <div class="card-left">
        <%= image_tag @post.photos.first.image.url(:medium), class: "card-img-top" %>
      </div>
    </div>
    <div class="col-md-4">
      <div class="card-right">
        <div class="card-right-comment">
          <div class="card-right-name">
            <%= link_to user_path(@post.user), class: "no-text-decoration" do %>
              <%= image_tag avatar_url(@post.user), class: "post-profile-icon" %>
            <% end %>
            <%= link_to user_path(@post.user), class: "black-color no-text-decoration post-user-name",
              title: @post.user.name do %>
              <strong><%= @post.user.name %></strong>
            <% end %>
          </div>
          <div class="m-2">
            <strong>
              <%= @post.caption %>
            </strong>
          </div>
          <div class="comment-post-id">
            <div class="m-2">
            </div>
          </div>
        </div>
        <div class="row parts">
        </div>
        <div class="post-time"><%= time_ago_in_words(@post.created_at).upcase %>前</div>
        <hr>
      </div>
    </div>
  </div>
</div>

追加

.
.
.
.post-wrap .col-md-8 {
  padding: 0px;
}

.card-right {
  padding: 20px;
}

.card-right-name {
  padding-bottom: 10px;
  border-bottom: 1px solid #e6e6e6;
  display: flex;
}

.card-right-comment {
  padding-bottom: 10px;
  border-bottom: 1px solid #e6e6e6;
  height: 320px;
  overflow: auto;
}

.postShow-wrap {
  border: 1px solid #e6e6e6;
  margin: 40px;
}

.post-user-name {
  display: flex;
  align-items: center;
  line-height: 0;
}

投稿詳細ページに遷移するリンクを追加

名前付きヘルパーがpostなので、末尾に_pathをつけて、post_pathとしてあげればlink_toメソッドでページ遷移先を指定できます。
ただ投稿詳細ページというのは、各投稿によって違うページが表示されます。
なのでpost_pathに引数を渡し、どの投稿の詳細ページに遷移するか指定する必要があります。

<% @posts.each do |post| %>
  <div class="col-md-8 col-md-2 mx-auto">
    <div class="card-wrap">
      <div class="card">
        <div class="card-header align-items-center d-flex">
          <%= link_to user_path(post.user), class: "no-text-decoration" do %>
            <%= image_tag avatar_url(post.user), class: "post-profile-icon" %>
          <% end %>
          <%= link_to user_path(post.user), class: "black-color no-text-decoration",
            title: post.user.name do %>
            <strong><%= post.user.name %></strong>
          <% end %>
        </div>

        <%# ==========ここから編集する========== %>
        <%= link_to(post_path(post)) do %>
          <%= image_tag post.photos.first.image.url(:medium), class: "card-img-top" %>
        <% end %>
        <%# ==========ここまで編集する========== %>

        <div class="card-body">
          <div class="row parts">
            <%= link_to "", "#", class: "love" %>
            <%= link_to "", "#", class: "comment" %>
          </div>
          <div><strong>「いいね!」10件</strong></div>
          <div>
            <span><strong><%= post.user.name %></strong></span>
            <span><%= post.caption %></span>
            <%= link_to time_ago_in_words(post.created_at).upcase + "前", "#", class: "post-time no-text-decoration" %>
            <hr>
            <div class="row parts">
              <form action="#" class="w-100">
                <div>
                  <textarea class="form-control comment-input border-0" placeholder="コメント..." rows="1"></textarea>
                </div>
              </form>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
<% end %>

投稿日時をクリックしても、投稿詳細ページに遷移するように実装します。posts/index.html.erbを以下のように編集してください。

<% @posts.each do |post| %>
  <div class="col-md-8 col-md-2 mx-auto">
    <div class="card-wrap">
      <div class="card">
        <div class="card-header align-items-center d-flex">
          <%= link_to user_path(post.user), class: "no-text-decoration" do %>
            <%= image_tag avatar_url(post.user), class: "post-profile-icon" %>
          <% end %>
          <%= link_to user_path(post.user), class: "black-color no-text-decoration",
            title: post.user.name do %>
            <strong><%= post.user.name %></strong>
          <% end %>
        </div>

        <%= link_to(post_path(post)) do %>
          <%= image_tag post.photos.first.image.url(:medium), class: "card-img-top" %>
        <% end %>

        <div class="card-body">
          <div class="row parts">
            <%= link_to "", "#", class: "love" %>
            <%= link_to "", "#", class: "comment" %>
          </div>
          <div><strong>「いいね!」10件</strong></div>
          <div>
            <span><strong><%= post.user.name %></strong></span>
            <span><%= post.caption %></span>

            <%# =====この行を編集する====== %>
            <%= link_to time_ago_in_words(post.created_at).upcase + "前", post_path(post), class: "post-time no-text-decoration" %>

            <hr>
            <div class="row parts">
              <form action="#" class="w-100">
                <div>
                  <textarea class="form-control comment-input border-0" placeholder="コメント..." rows="1"></textarea>
                </div>
              </form>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
<% end %>

投稿削除機能

ルーティングの設定

Rails.application.routes.draw do
  devise_for :users,
    controllers: { registrations: 'registrations' }

  root 'posts#index'

  get '/users/:id', to: 'users#show', as: 'user'

  # この行を編集する
  resources :posts, only: %i(index new create show destroy) do
    resources :photos, only: %i(create)
  end
end

コントローラーの編集

  .
  .
  .
  def show
    @post = Post.find_by(id: params[:id])
  end
  
  # ==========ここから追加する==========
  def destroy
    @post = Post.find_by(id: params[:id])
    if @post.user == current_user
      flash[:notice] = "投稿が削除されました" if @post.destroy
    else
      flash[:alert] = "投稿の削除に失敗しました"
    end
    redirect_to root_path
  end
  # ==========ここまで追加する==========

  private
    def post_params
      params.require(:post).permit(:caption, photos_attributes: [:image]).merge(user_id: current_user.id)
    end
end

受け取ったHTTPリクエストからidを判別し、指定の投稿1つを@postに代入しています。

リファクタリング

重複するコードがあると冗長なので、before_actionを使ってshowアクションとdestroyアクションが呼ばれる前に@postを読み込むように書き換えます。

class PostsController < ApplicationController
  before_action :authenticate_user!

  # この行を追加する
  before_action :set_post, only: %i(show destroy)
  .
  .
  .
  # @post = Post.find_by(id: params[:id]) を削除する
  def show
  end
  
  # @post = Post.find_by(id: params[:id]) を削除する
  def destroy
    if @post.user == current_user
      flash[:notice] = "投稿が削除されました" if @post.destroy
    else
      flash[:alert] = "投稿の削除に失敗しました"
    end
    redirect_to root_path
  end

  private
    def post_params
      params.require(:post).permit(:caption, photos_attributes: [:image]).merge(user_id: current_user.id)
    end

    # set_postというメソッドを追加する
    def set_post
      @post = Post.find_by(id: params[:id])
    end
end

ビューを編集

<% @posts.each do |post| %>
  <div class="col-md-8 col-md-2 mx-auto">
    <div class="card-wrap">
      <div class="card">
        <div class="card-header align-items-center d-flex">
          <%= link_to user_path(post.user), class: "no-text-decoration" do %>
            <%= image_tag avatar_url(post.user), class: "post-profile-icon" %>
          <% end %>
          <%= link_to user_path(post.user), class: "black-color no-text-decoration",
            title: post.user.name do %>
            <strong><%= post.user.name %></strong>
          <% end %>

          <%# ==========ここから追加する========== %>
          <% if post.user_id == current_user.id %>
            <%= link_to post_path(post), method: :delete, class: "ml-auto mx-0 my-auto" do %>
              <div class="delete-post-icon">
              </div>
            <% end %>
          <% end %>
          <%# ==========ここから追加する========== %>

        </div>
        .
        .
        .
.
.
.
 /* ここから追加 */
.delete-post-icon {
  background-image: url("~parts9");
  background-repeat: no-repeat;
  width: 20px;
  height: 20px;
  background-size: 20px !important;
  color: #262626;
  font-size: 20px;
}

次に投稿詳細ページからも投稿を削除できるように実装します。

<div class="col-md-10 col-md-offset-1 mx-auto postShow-wrap">
  <div class="row post-wrap">
    <div class="col-md-8">
      <div class="card-left">
        <%= image_tag @post.photos.first.image.url(:medium), class: "card-img-top" %>
      </div>
    </div>
    <div class="col-md-4">
      <div class="card-right">
        <div class="card-right-comment">
          <div class="card-right-name">
            <%= link_to user_path(@post.user), class: "no-text-decoration" do %>
              <%= image_tag avatar_url(@post.user), class: "post-profile-icon" %>
            <% end %>
            <%= link_to user_path(@post.user), class: "black-color no-text-decoration post-user-name",
              title: @post.user.name do %>
              <strong><%= @post.user.name %></strong>
            <% end %>

            <%# ==========ここから追加する========== %>
            <% if @post.user_id == current_user.id %>
              <%= link_to post_path(@post), method: :delete, class: "ml-auto mx-0 my-auto" do %>
                <div class="delete-post-icon">
                </div>
              <% end %>
            <% end %>
            <%# ==========ここまで追加する========== %>

          </div>
          .
          .
          .

6. いいね機能

モデル作成

モデルの作成

$ rails g model Like
class CreateLikes < ActiveRecord::Migration[6.0]
  def change
    create_table :likes do |t|
      # ==========ここから追加する==========
      t.references :post, foreign_key: true, null: false
      t.references :user, foreign_key: true, null: false
      # ==========ここまで追加する==========
      t.timestamps
    end
  end
end
$ rails db:migrate

アソシエーションの設定

モデルを作成したらアソシエーションの設定を行います。
アソシエーションとは、2つのモデル同士のつながりを指します。モデルとモデルの間には関連付けを行う必要があります。

UserモデルとLikeモデルのアソシエーションの設定

class User < ApplicationRecord
  has_many :posts, dependent: :destroy

  # この行を追加する
  has_many :likes
  .
  .
  .

has_manyは、他のモデルとの間に「1対多」のつながりがあることを示します。(「1側」にhas_manyを追加します。)
has_manyが使用されている場合、「反対側」のモデルでは多くの場合belongs_toが使われます。

class Like < ApplicationRecord

  # この行を追加する
  belongs_to :user

end

PostモデルとLikeモデルのアソシエーション設定

class Post < ApplicationRecord
  belongs_to :user
  has_many :photos, dependent: :destroy

  # この行を追加する
  has_many :likes, -> { order(created_at: :desc) }, dependent: :destroy

  accepts_nested_attributes_for :photos
end

dependent: :destroyをつけることで、オブジェクトが削除されるときに、関連付けられたオブジェクトのdestroyメソッドが実行されます。
つまり今回で言うと、投稿が削除されたら、その投稿に紐づくいいねも削除します。

class Like < ApplicationRecord
  belongs_to :user

  # この行を追加する
  belongs_to :post

end

バリデーションの設定

class Like < ApplicationRecord
  belongs_to :user
  belongs_to :post

  # ここの行を追加する
  validates :user_id, uniqueness: { scope: :post_id }
end

uniquenessは、オブジェクトが保存される直前に、属性の値が一意(unique)であり重複していないことを検証します。
つまりuser_idとpost_idの組み合わせが重複していないことを検証します。

いいね機能実装

ルーティングの追加

Rails.application.routes.draw do
  devise_for :users,
    controllers: { registrations: 'registrations' }

  root 'posts#index'

  get '/users/:id', to: 'users#show', as: 'user'

  resources :posts, only: %i(index new create show destroy) do
    resources :photos, only: %i(create)

    # この行を追加する
    resources :likes, only: %i(create destroy)
  end
end

ルーティングをネスト(入れ子)にすることで、親子関係をルーティングで表すことができます。
likesに関してはpostsに対して「子」の関係になるので、ネストすることで、どの投稿に紐づくかを明示できます。

コントローラーの作成

いいね機能で使うコントローラーを作成します。コントローラーの名称は先程設定したルーティングでlikesと指定したので、likesコントローラーを作成していきます。

作成したコントローラーにアクションを追加

create

class LikesController < ApplicationController
  def create
    @like = current_user.likes.build(like_params)
    @post = @like.post
    if @like.save
      respond_to :js
    end
  end

  private
    def like_params
      params.permit(:post_id)
    end
end

このコードはbuildメソッドを使って、インスタンスを作成しています。
またlike_paramsというメソッドを引数で呼び出しています。

respond_toは返却するレスポンスのフォーマットを切り替えるためのメソッドです。

destroy

次にlikes#destroyなので、likesコントローラーにdestroyアクションを作成します。

class LikesController < ApplicationController
  def create
    @like = current_user.likes.build(like_params)
    @post = @like.post
    if @like.save
      respond_to :js
    end
  end

  # ==========ここから追加する==========
  def destroy
    @like = Like.find_by(id: params[:id])
    @post = @like.post
    if @like.destroy
      respond_to :js
    end
  end
  # ==========ここまで追加する==========

  private
    def like_params
      params.permit(:post_id)
    end
end

ユーザーがサインインしていない状態でいいねをしても、誰がいいねしたか分からないです。
なのでauthenticate_user!を使ってサインイン済みユーザーのみにアクセス許可を与えるようにします。

class LikesController < ApplicationController

  # ここに追加する
  before_action :authenticate_user!

  def create
    @like = current_user.likes.build(like_params)
    @post = @like.post
    if @like.save
      respond_to :js
    end
  end
  .
  .
  .

ビューを作成

ビュー編集

<% @posts.each do |post| %>
  <div class="col-md-8 col-md-2 mx-auto">
    <div class="card-wrap">
      <div class="card">
        <div class="card-header align-items-center d-flex">
          <%= link_to user_path(post.user), class: "no-text-decoration" do %>
            <%= image_tag avatar_url(post.user), class: "post-profile-icon" %>
          <% end %>
          <%= link_to user_path(post.user), class: "black-color no-text-decoration",
            title: post.user.name do %>
            <strong><%= post.user.name %></strong>
          <% end %>

          <% if post.user_id == current_user.id %>
            <%= link_to post_path(post), method: :delete, class: "ml-auto mx-0 my-auto" do %>
              <div class="delete-post-icon">
              </div>
            <% end %>
          <% end %>

        </div>

        <%= link_to(post_path(post)) do %>
          <%= image_tag post.photos.first.image.url(:medium), class: "card-img-top" %>
        <% end %>

        <div class="card-body">
          <div class="row parts">

            <%# ==========ここから編集する========== %>
            <div id="like-icon-post-<%= post.id.to_s %>">
              <% if post.liked_by(current_user).present? %>
                <%= link_to "いいねを取り消す", post_like_path(post.id, post.liked_by(current_user)), method: :DELETE, remote: true, class: "loved hide-text" %>
              <% else %>
                <%= link_to "いいね", post_likes_path(post), method: :POST, remote: true, class: "love hide-text" %>
              <% end %>
            </div>
            <%# ==========ここまで編集する========== %>

            <%= link_to "", "#", class: "comment" %>
          </div>

          <%# ==========ここから編集する========== %>
          <div id="like-text-post-<%= post.id.to_s %>">
            <%= render "like_text", { likes: post.likes } %>
          </div>
          <%# ==========ここまで編集する========== %>

          <div>
            <span><strong><%= post.user.name %></strong></span>
            <span><%= post.caption %></span>
            <%= link_to time_ago_in_words(post.created_at).upcase + "前", post_path(post), class: "post-time no-text-decoration" %>
            <hr>
            <div class="row parts">
              <form action="#" class="w-100">
                <div>
                  <textarea class="form-control comment-input border-0" placeholder="コメント..." rows="1"></textarea>
                </div>
              </form>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
<% end %>

liked_byというメソッドを使っていますが、こちらはまだモデルに定義していないので、定義します。

class Post < ApplicationRecord
  belongs_to :user
  has_many :photos, dependent: :destroy
  has_many :likes, dependent: :destroy

  accepts_nested_attributes_for :photos

  # ==========ここから追加する==========
  def liked_by(user)
    # user_idとpost_idが一致するlikeを検索する
    Like.find_by(user_id: user.id, post_id: id)
  end
  # ==========ここまで追加する==========

end

user_idとpost_idが一致するlikeを探し、なければnilを返します。

liked_byの引数にcurrent_userを渡すことで、投稿にサインインしているユーザーのいいねがあるかどうかを判断します。

次にapp/views/posts/のフォルダの中に_like_text.html.erbというファイルを作成して以下のコードを追加してください。

<strong>
  <% likes.each.with_index do |like, index| %>
    <% if likes.size == 1 %>
      <%= like.user.name %> </strong> が「いいね!」しました
    <% elsif like == likes.last %>
      </strong>and<strong>
      <%= + like.user.name %></strong> が「いいね!」しました
    <% elsif index > 1 %>
      </strong><%= "and " + (likes.size-index).to_s + " 他 " %> が「いいね!」しました
      <% break %>
    <% elsif index == likes.size-2 || index == 1 %>
      <%= like.user.name %>
    <% else %>
      <%= like.user.name + ", " %>
    <% end %>
  <% end %>
</strong>
.
.
.
.loved {
  background-image: url("~parts7");
  background-repeat: no-repeat;
  height: 36px;
  width: 36px;
  background-size: 36px !important;
}

.hide-text {
  display: block;
  overflow: hidden;
  text-indent: 110%;
  white-space: nowrap;
}

次にapp/views/likes/のフォルダの中にcreate.js.erbというファイルを作成します。

likesコントローラーのcreateアクションでrespond_to :jsというコードを追加しました。
フォーマットをJS形式にした場合、出力するファイルはapp/views/コントローラ名/アクション名.js.erbになります。

なので、create.js.erbファイルに以下のコードを追加します。

$('#like-icon-post-<%= @post.id.to_s %>').
  html('<%= link_to "いいね", post_like_path(@post.id, @like), method: :DELETE, remote: true, class: "loved hide-text" %>');
$('#like-text-post-<%= @post.id.to_s %>').
  html('<%= j render "posts/like_text", { likes: @post.likes } %>');

.html('htmlString')というコードで、マッチした1つ目の要素のHTMLを変更します。今回でいうと、いいねを押したら、いいねを取り消すHTMLを表示するようにしています。

同様に、app/views/likesフォルダの中にdestroy.js.erbというファイルを作成して以下のコードを追加してください。

$('#like-icon-post-<%= @post.id.to_s %>').
  html('<%= link_to "いいねを取り消す", post_likes_path(@post), method: :POST, remote: true, class: "love hide-text" %>');
$('#like-text-post-<%= @post.id.to_s %>').
  html('<%= j render "posts/like_text", { likes: @post.likes } %>');

投稿詳細ページのビューを編集

app/vies/posts/posts/show.html.erb
<div class="col-md-10 col-md-offset-1 mx-auto postShow-wrap">
<div class="row post-wrap">
<div class="col-md-8">
<div class="card-left">
<%= image_tag @post.photos.first.image.url(:medium), class: "card-img-top" %>
</div>
</div>
<div class="col-md-4">
<div class="card-right">
<div class="card-right-comment">
<div class="card-right-name">
<%= link_to user_path(@post.user), class: "no-text-decoration" do %>
<%= image_tag avatar_url(@post.user), class: "post-profile-icon" %>
<% end %>
<%= link_to user_path(@post.user), class: "black-color no-text-decoration post-user-name",
title: @post.user.name do %>
<strong><%= @post.user.name %></strong>
<% end %>

        <% if @post.user_id == current_user.id %>  
          <%= link_to post_path(@post), method: :delete, class: "ml-auto mx-0 my-auto" do %>  
            <div class="delete-post-icon">  
            </div>  
          <% end %>  
        <% end %>  

      </div>  
      <div class="m-2">  
        <strong>  
          <%= @post.caption %>  
        </strong>  
      </div>  
      <div class="comment-post-id">  
        <div class="m-2">  
        </div>  
      </div>  
    </div>  
    <div class="row parts">  

      <%# ==========ここから追加する========== %>  
      <div id="like-icon-post-<%= @post.id.to_s %>">  
        <% if @post.liked_by(current_user).present? %>  
          <%= link_to "いいねを取り消す", post_like_path(@post.id, @post.liked_by(current_user)), method: :DELETE, remote: true, class: "loved hide-text" %>  
        <% else %>  
          <%= link_to "いいね", post_likes_path(@post), method: :POST, remote: true, class: "love hide-text" %>  
        <% end %>  
      </div>  
      <%# ==========ここまで追加する========== %>  

    </div>  

    <%# ==========ここから追加する========== %>  
    <div id="like-text-post-<%= @post.id.to_s %>">  
      <%= render "like_text", { likes: @post.likes } %>  
    </div>  
    <%# ==========ここまで追加する========== %>  

    <div class="post-time"><%= time_ago_in_words(@post.created_at).upcase %>前</div>  
    <hr>  
  </div>  
</div>  

</div>
</div>


いいねを削除するためには、そのいいねに紐づいている投稿の情報が必要になります。
また投稿には複数いいねをつけることができます。その複数のいいねのうち、どのいいねなのかを認識するために、いいねの情報も必要です。
なのでpost_likeに2つの引数を渡し、どの投稿の、どのいいねを削除するのかを指定する必要があります。
今回のコードの場合、@post.idが投稿のidになります。
また@post.liked_by(current_user)が現在ログインしているユーザーがいいねした投稿のidを指定しています。


# 7. コメント機能

## モデル作成

### モデルの作成

$ rails g model Comment


```db/migrate/xxxxxx_create_comments.rb  
class CreateComments < ActiveRecord::Migration[6.0]  
  def change  
    create_table :comments do |t|  
  
      # ==========ここから追加する==========  
      t.text :comment, null: false  
      t.references :post, foreign_key: true, null: false  
      t.references :user, foreign_key: true, null: false  
      # ==========ここまで追加する==========  
  
      t.timestamps  
    end  
  end  
end  
$ rails db:migrate  

アソシエーションの設定

UserモデルとCommentモデルのアソシエーションの設定

class User < ApplicationRecord  
  has_many :posts, dependent: :destroy  
  has_many :likes  
  
  # この行を追加する  
  has_many :comments  
  .  
  .  
  .  
class Comment < ApplicationRecord  
  
  # この行を追加する  
  belongs_to :user  
  
end  

PostモデルとCommentモデルのアソシエーション設定

class Post < ApplicationRecord  
  belongs_to :user  
  has_many :photos, dependent: :destroy  
  has_many :likes, dependent: :destroy  
  
  # この行を追加する  
  has_many :comments, dependent: :destroy  
  
  accepts_nested_attributes_for :photos  
  
  def liked_by(current_user)  
    # user_idが一致するlikeを検索する  
    Like.find_by(user_id: current_user.id, post_id: id)  
  end  
end  
class Comment < ApplicationRecord  
  belongs_to :user  
  
  # この行を追加する  
  belongs_to :post  
end  

コメント機能実装

ルーティングの追加

Rails.application.routes.draw do  
  devise_for :users,  
    controllers: { registrations: 'registrations' }  
  
  root 'posts#index'  
  
  get '/users/:id', to: 'users#show', as: 'user'  
  
  resources :posts, only: %i(index new create show destroy) do  
    resources :photos, only: %i(create)  
    resources :likes, only: %i(create destroy)  
  
    # ここに追加する  
    resources :comments, only: %i(create destroy)  
  end  
end  

コントローラーの作成

$ rails g controller comments  

作成したコントローラーにアクションを追加

  
  # ここに追加する  
  before_action :authenticate_user!  
  
  def create  
    @comment = Comment.new(comment_params)  
    @post = @comment.post  
    if @comment.save  
      respond_to :js  
    else  
      flash[:alert] = "コメントに失敗しました"  
    end  
  end  
  
  # ==========ここから追加する==========  
  def destroy  
    @comment = Comment.find_by(id: params[:id])  
    @post = @comment.post  
    if @comment.destroy  
      respond_to :js  
    else  
      flash[:alert] = "コメントの削除に失敗しました"  
    end  
  end  
  # ==========ここまで追加する==========  
  
  private  
    def comment_params  
      params.required(:comment).permit(:user_id, :post_id, :comment)  
    end  
end  

ビューを作成

<% @posts.each do |post| %>  
  <div class="col-md-8 col-md-2 mx-auto">  
    <div class="card-wrap">  
      <div class="card">  
        <div class="card-header align-items-center d-flex">  
          <%= link_to user_path(post.user), class: "no-text-decoration" do %>  
            <%= image_tag avatar_url(post.user), class: "post-profile-icon" %>  
          <% end %>  
          <%= link_to user_path(post.user), class: "black-color no-text-decoration",  
            title: post.user.name do %>  
            <strong><%= post.user.name %></strong>  
          <% end %>  
  
          <% if post.user_id == current_user.id %>  
            <%= link_to post_path(post), method: :delete, class: "ml-auto mx-0 my-auto" do %>  
              <div class="delete-post-icon">  
              </div>  
            <% end %>  
          <% end %>  
  
        </div>  
  
        <%= link_to(post_path(post)) do %>  
          <%= image_tag post.photos.first.image.url(:medium), class: "card-img-top" %>  
        <% end %>  
  
        <div class="card-body">  
          <div class="row parts">  
  
            <div id="like-icon-post-<%= post.id.to_s %>">  
              <% if post.liked_by(current_user).present? %>  
                <%= link_to "いいねを取り消す", post_like_path(post.id, post.liked_by(current_user)), method: :DELETE, remote: true, class: "loved hide-text" %>  
              <% else %>  
                <%= link_to "いいね", post_likes_path(post), method: :POST, remote: true, class: "love hide-text" %>  
              <% end %>  
            </div>  
  
            <%= link_to "", "#", class: "comment" %>  
          </div>  
  
          <div id="like-text-post-<%= post.id.to_s %>">  
            <%= render "like_text", { likes: post.likes } %>  
          </div>  
  
          <div>  
            <span><strong><%= post.user.name %></strong></span>  
            <span><%= post.caption %></span>  
            <%= link_to time_ago_in_words(post.created_at).upcase + "前", post_path(post), class: "post-time no-text-decoration" %>  
            
            <%# ==========ここから編集する========== %>  
            <div id="comment-post-<%= post.id.to_s %>">  
              <%= render 'comment_list', { post: post } %>  
            </div>  
            <%= link_to time_ago_in_words(post.created_at).upcase + "前", post_path(post),  
              class: "light-color post-time no-text-decoration" %>  
            <hr>  
            <div class="row actions" id="comment-form-post-<%= post.id.to_s %>">  
              <%= form_with model: [post, Comment.new], class: "w-100" do |f| %>  
                <%= f.hidden_field :user_id, value: current_user.id %>  
                <%= f.hidden_field :post_id, value: post.id %>  
                <%= f.text_field :comment, class: "form-control comment-input border-0", placeholder: "コメント ...", autocomplete: :off %>  
              <% end %>  
            </div>  
            <%# ==========ここまで編集する========== %>  
  
          </div>  
        </div>  
      </div>  
    </div>  
  </div>  
<% end %>  

app/views/postsフォルダの中に_comment_list.html.erbファイルを作成して以下のコードを追加してください。

<% post.comments.each do |comment| %>  
  <div class="mb-2">  
    <% if comment.user == current_user %>  
      <%= link_to "", post_comment_path(post.id, comment), method: :delete, remote: true, class: "delete-comment" %>  
    <% end %>  
    <span>  
      <strong>  
        <%= link_to comment.user.name, user_path(comment.user), class: "no-text-decoration black-color" %>  
      </strong>  
    </span>  
    <span><%= comment.comment %></span>  
  </div>  
<% end %>  

scss 編集

.  
.  
.  
.delete-comment {  
  background-image: url("~parts8");  
  background-repeat: no-repeat;  
  width: 11px;  
  height: 11px;  
  float: right;  
  margin: 5px 0 0 10px;  
  background-size: 11px !important;  
}  

app/views/comments/のフォルダの中にcreate.js.erbというファイルを作成します。

commentsコントローラーのcreateアクションでrespond_to :jsというコードを追加しました。
フォーマットをJS形式にした場合、出力するファイルはapp/views/コントローラ名/アクション名.js.erbになります。

$('#comment-post-<%= @post.id.to_s %>').  
  html('<%= j render "posts/comment_list", { post: @post } %>');  
$('#comment-form-post-<%= @post.id.to_s %> #comment_comment').  

↑valはHTMLタグ内に記述されているvalue属性を取得したり変更できるメソッドです。
 今回val("")とすることで、入力したコメントの内容(value)を空にしています。

$('#comment-post-<%= @post.id.to_s %>').  
  html('<%= j render "posts/comment_list", { post: @post } %>');  

投稿詳細ページのビューを編集

app/views/posts/show.html.erb
<div class="col-md-10 col-md-offset-1 mx-auto postShow-wrap">
<div class="row post-wrap">
<div class="col-md-8">
<div class="card-left">
<%= image_tag @post.photos.first.image.url(:medium), class: "card-img-top" %>
</div>
</div>
<div class="col-md-4">
<div class="card-right">
<div class="card-right-comment">
<div class="card-right-name">
<%= link_to user_path(@post.user), class: "no-text-decoration" do %>
<%= image_tag avatar_url(@post.user), class: "post-profile-icon" %>
<% end %>
<%= link_to user_path(@post.user), class: "black-color no-text-decoration post-user-name",
title: @post.user.name do %>
<strong><%= @post.user.name %></strong>
<% end %>

        <% if @post.user_id == current_user.id %>
          <%= link_to post_path(@post), method: :delete, class: "ml-auto mx-0 my-auto" do %>
            <div class="delete-post-icon">
            </div>
          <% end %>
        <% end %>

      </div>
      <div class="m-2">
        <strong>
          <%= @post.caption %>
        </strong>
      </div>
      <div class="comment-post-id">
        <div class="m-2">

          <%# ==========ここから追加する========== %>
          <div id="comment-post-<%= @post.id.to_s %>">
            <%= render 'comment_list', post: @post %>
          </div>
          <%# ==========ここまで追加する========== %>

        </div>
      </div>
    </div>
    <div class="row parts">

      <div id="like-icon-post-<%= @post.id.to_s %>">
        <% if @post.liked_by(current_user).present? %>
          <%= link_to "いいねを取り消す", post_like_path(@post.id, @post.liked_by(current_user)), method: :DELETE, remote: true, class: "loved hide-text" %>
        <% else %>
          <%= link_to "いいね", post_likes_path(@post), method: :POST, remote: true, class: "love hide-text" %>
        <% end %>
      </div>

    </div>

    <div id="like-text-post-<%= @post.id.to_s %>">
      <%= render "like_text", { likes: @post.likes } %>
    </div>

    <div class="post-time"><%= time_ago_in_words(@post.created_at).upcase %>前</div>
    <hr>

    <%# ==========ここから追加する========== %>
    <div class="row parts" id="comment-form-post-<%= @post.id.to_s %>">
      <%= form_with model: [@post, Comment.new],  class: "w-100" do |f| %>
        <%= f.hidden_field :user_id, value: current_user.id %>
        <%= f.hidden_field :post_id, value: @post.id %>
        <%= f.text_field :comment, class: "form-control comment-input border-0", placeholder: "コメント ...", autocomplete: :off %>
      <% end %>
    </div>
    <%# ==========ここまで追加する========== %>

  </div>
</div>

</div>
</div>

Discussion