住所自動入力機能(jpostalとjp_prefectureの導入)
内容
住所登録/検索機能を実装する
住所登録機能
投稿時に住所(都道府県、市区町村)も一緒に登録できるようにする
postsテーブルにaddressesテーブルを紐付けるため、Formオブジェクトパターンを使用して投稿時に複数のテーブルに保存できるようにする
ちなみに入力は、jpostalとjp_prefectureというgemを用いて住所自動入力を実装する
1. jp_prefectureの導入
jquery-railsとjp_prefectureを導入する
今回は既にjQueryは導入済みのため省略
Gemfile
gem 'jp_prefecture'
ターミナル
% bundle install % rails s
2. モデルを作成し、マイグレーションファイルを編集
Postモデルは作成済みのため、Addressモデルのみ作成し、マイグレーションファイルを編集する
ターミナル
% rails g model address
db/migrate/**************_create_addresses.rb
create_table :addresses do |t| t.integer :postal_code, null: false t.integer :prefecture_code, null: false t.string :city, null: false t.string :street, null: false t.references :post, null: false, foreign_key: true t.timestamps end
ターミナル
% rails db:migrate
3. アソシエーションを記述
app/models/post.rb
has_one :address, dependent: :destroy
app/models/address.rb
belongs_to :post
4. モデルを編集
公式サイトを参考にモデルを編集する
JpPrefecture
address.rb
include JpPrefecture jp_prefecture :prefecture_code def prefecture_name JpPrefecture::Prefecture.find(code: prefecture_code).try(:name) end def prefecture_name=(prefecture_name) self.prefecture_code = JpPrefecture::Prefecture.find(name: prefecture_name).code end
5. 新たにmodelsディレクトリ直下にファイルを作成し、クラスを定義
app/modelsディレクトリ配下にpost_address.rbを作成
post_address.rb
class PostAddress include ActiveModel::Model attr_accessor :map_link, :distance, :course, :slope, :traffic, :crowd, :view, :comment, :user_id, :postal_code, :prefecture_code, :city, :street with_options presence: true do validates :map_link, format: { with: /\A#{URI::DEFAULT_PARSER.make_regexp(%w[http https])}\z/, message: 'はhttpもしくはhttpsで始まるURLで入力してください' } validates :distance, numericality: { greater_than: 0 } validates :course validates :slope validates :traffic validates :crowd validates :view validates :comment, length: { maximum: 140 } validates :user_id validates :postal_code, format: { with: /\A[0-9]{7}\z/ } validates :prefecture_code validates :city validates :street end end
postsテーブルに保存されるuser_idには、本来belongs_to :userのアソシエーションにより、バリデーションが設定されている
しかし、post_addressクラスにはアソシエーションを定義することはできないため、belongs_toによるバリデーションを行うことができない
そこで、post_addressクラスでuser_idに対してバリデーションを新たに設定
6. 元々記述していたpost.rbのバリデーションを削除
7. データをテーブルに保存する処理を記述
post_address.rb
class PostAddress # 略 def save post = Post.create(map_link: map_link, distance: distance, course: course, slope: slope, traffic: traffic, crowd: crowd, view: view, comment: comment, user_id: user_id) Address.create(postal_code: postal_code, prefecture_code: prefecture_code, city: city, street: street, post_id: post.id) end end
8. コントローラーを編集
postsコントローラーを以下のように編集する
posts_controller.rb
def new @post_address = PostAddress.new end def create @post_address = PostAddress.new(post_params) if @post_address.valid? @post_address.save redirect_to posts_path else render :new end end def post_params params.require(:post_address).permit(:map_link, :distance, :course, :slope, :traffic, :crowd, :view, :comment, :image, :postal_code, :prefecture_code, :city, :street).merge(user_id: current_user.id) end
valid?メソッドを使用しているのは、PostAddressクラスがApplicationRecordを継承していないことにより、saveメソッドにはバリデーションを実行する機能がないため
9. ビューを編集(新規投稿ページ)
生成したインスタンスをnew.html.erbにおいて利用
app/views/posts/new.html.erb
# 修正前 <%= form_with model: @post, local: true do |f| %> # 修正後 <%= form_with model: @post_address, local: true do |f| %> # 下記追加 <div class="weight-bold-text"> 郵便番号(ハイフンなし) <span class="indispensable">必須</span> </div> <%= f.text_field :postal_code, autocomplete: 'postal_code', class: "form-control", id: 'postal_code' %> <div class="weight-bold-text"> 都道府県 <span class="indispensable">必須</span> </div> <%= f.collection_select :prefecture_code, JpPrefecture::Prefecture.all, :code, :name, { prompt: '選択してください' }, class: 'form-control', id: 'prefecture_code' %> <div class="weight-bold-text"> 市区町村 <span class="indispensable">必須</span> </div> <%= f.text_field :city, autocomplete: 'city', class: "form-control", id: 'city' %> <div class="weight-bold-text"> 番地 <span class="indispensable">必須</span> </div> <%= f.text_field :street, autocomplete: 'street', class: "form-control", id: 'street' %>
10. jquery.jpostal.jsを導入
公式GitHubを参考に導入
jquery.jpostal.js
設置方法は2種類あり、自分のサーバに郵便データを設置する方法としない方法がある
jpostal-1006.appspot.comで公開しているので、jquery.jpostal.jsやjson/*.jsonを設置する必要がない、サイト運営者の定期的な郵便データ更新作業も必要ない、と記載されているので今回は自分のサーバにはデータを設置しない方法で導入する(おそらくこちらの方が楽)
まず、jquery本体とjquery.jpostal.jsをインクルードする(今回はjqueryは導入済みのためインクルードの必要はなかった)
application.html.erb
<head> <script type="text/javascript" src="//jpostal-1006.appspot.com/jquery.jpostal.js"></script> </head>
app/javascript配下にaddress_autofill.jsを作成し、コードを記述
app/javascript/address_autofill.js
$(function(){ $('#postal_code').jpostal({ postcode: ['#postal_code'], address: { '#prefecture_code': '%3', '#city': '%4', '#street': '%5%6%7', } }); });
公式サイトではjQueryのreadyメソッドを使用しているが、現在は非推奨となっているため修正している
最後にapplication.jsでaddress_autofill.jsを読み込む
app/javascript/packs/address_autofill.js
require("../address_autofill")
以上で導入完了
11. Formオブジェクトのedit/update
ここは難しく説明ができないため割愛
12. Formオブジェクトのテストコード
所感
理解できていないコードを写すのはよくないと痛感。。。 でもできることも増やしていかないといけないので、やりながら理解していくしかない!
railsの日本語化
内容
投稿一覧ページとは別に、トップページを実装する
rails日本語化
1. エラーメッセージを日本語化
時刻を日本に合わせたため、ついでにエラーメッセージも日本語化する(application.rbのみ編集済み)
日本語に対応するgemを導入
Gemfile
gem 'rails-i18n' #devise日本語化 gem 'devise-i18n'
ターミナル
% bundle install % rails s % rails g devise:i18n:locale ja
config/localesディレクトリにファイルが作成され、自動的に日本語化してくれている
しかしこのままではGem 'rails-i18n'やdevise.ja.ymlを導入したことによって使える日本語しか含まれていないので、deviseのデフォルト以外の文字は英語で表示されてしまう
したがって、これらを翻訳するファイルを手動で作成
config/localesディレクトリに、ja.ymlというファイルは既に作成しているので、そこに追記していく
config/locales/ja.yml
例)以下のように日本語化されていない単語を日本語登録する ja: activerecord: attributes: user: nickname: ニックネーム post: comment: コメント comment: comment: コメント
モデルのバリデーションのメッセージも日本語にしておく
user.rb
# 例) validates :password, format: { with: /\A(?=.*?[a-z])(?=.*?\d)[a-z\d]+\z/i, message: 'は半角英数字混合で入力してください' }
2. テストコードを修正
テストコードのエラーメッセージの部分を日本語に修正
user_spec.rb
# 元 expect(@user.errors.full_messages).to include("Nickname can't be blank") # 日本語化 expect(@user.errors.full_messages).to include("ニックネームを入力してください")
修正できたらテストを実行して確かめる
ターミナル
% bundle exec rspec spec/models/user_spec.rb
コメント機能
Action Cableを用いてコメントの即時更新機能を実装する
理解に時間がかかりそうだったのと、このアプリにおいては必須ではないため、即時更新機能は保留とすることにした
トップページ機能
1. ルーティングを設定
ルートパスをindexからtopに変更する
routes.eb
root to: 'posts#top'
2. コントローラーを編集
postsコントローラーでtopアクションを定義
posts_controller.rb
before_action :authenticate_user!, except: %i[top index show search] def top end
3. ビューを編集
これまでroot_pathはindexアクションを呼び出していたが現在はtopアクションに変更されている
投稿が完了した時等の何かしらのアクションを起こした後は、indexアクションを呼び出し投稿一覧ページを表示させるようにする
root_path→posts_pathに修正
また、top.html.erbを作成し、ログインボタン等を記述
4. application_controller.rbを編集
deviseはサインアップやサインインした時、デフォルトでルートパスに遷移するように設定されている
トップページからログインすると投稿一覧ページに遷移させたいため、コントローラーを編集する
application_controller.rb
def after_sign_in_path_for(resource) posts_path end def after_sign_up_path_for(resource) posts_path end
次回
住所登録/検索機能を実装する
参考
CSSでボタンクリックの動きをつけたい時
押し込み式ボタンを作成した時、最初は以下のように記述していた
top.css
.top-btn { border-bottom: 0.2rem solid #800000; } .top-btn:active { transform: translateY(0.2rem); border-bottom: none; }
しかし、これだと前後の要素も一緒に動いてしまった
marginの相殺やらが悪さするらしいが、今回はmarginを特に指定していない
translateYを用いるとボタンの下がすべて動いてしまうのか?
いまいちわからないが以下のように編集すると問題なく動いた
top.css
.top-btn { position: relative; box-shadow: 0 0.2rem #800000; } .top-btn:active { top: 0.2rem; box-shadow: none; }
コメント機能
内容
投稿詳細ページでコメント機能を実装する
コメント機能
1. モデルを作成
Commentモデルを作成し、マイグレーションファイルを編集する
ターミナル
% rails g model comment
db/migrate/20XXXXXXXXXXXX_create_comments.rb
t.text :comment t.references :user, null: false, foreign_key: true t.references :post, null: false, foreign_key: true
ターミナル
% rails db:migrate
続いてアソシエーションを記述
app/models/comment.rb
belongs_to :user belongs_to :post
app/models/user.rb
has_many :comments, dependent: :destroy
app/models/post.rb
has_many :comments, dependent: :destroy
2. ルーティングを設定
コメントは投稿に紐付くのでルーティングにネストを利用する
routes.rb
resources :posts, except: :index do resources :comments, only: %i[create destroy] end
※いいね機能の時はネストをresource :likeで記述してよかった
→なぜなら1件の投稿に対していいねは1回しかできないため、idを指定しなくても投稿に関連したいいねを削除できるから
※一方、コメント機能ではresources :commentsとしなければならない
→なぜなら1件の投稿に対してコメントは何回でもでき、それを削除したい場合どのコメントを消すかidを指定しなければならないから
3. コントローラーを作成
commentsコントローラーを作成し、createアクションとdestroyアクションを定義
ターミナル
% rails g controller comments
app/controllers/comments_controller.rb
before_action :post_find, only: %i[create destroy] def create @comment = @post.comments.new(comment_params) if @comment.save redirect_to post_path(@post.id) else @comments = @post.comments.includes(:user) render post_path(@post.id) end end def destroy @comment = @post.comments.find(params[:id]) @comment.destroy redirect_to post_path(@post.id) end private def post_find @post = Post.find(params[:post_id]) end def comment_params params.require(:comment).permit(:comment).merge(user_id: current_user.id) end
4. ビューを編集
投稿詳細ページにコメント投稿フォーム、およびコメント表示欄を作成
app/views/posts/show.html.erb
<%= form_with(model: [@post, @comment], local: true) do |f| %> <%= f.text_area :comment, placeholder: "コメントする", maxlength: "140" %> <%= f.submit "送信" %> <% end %> <% @comments.each do |comment| %> <p> <strong><%= link_to comment.user.nickname, user_path(comment.user.id) %>:</strong> <%= comment.comment %> <strong>[<%= link_to "削除", post_comment_path(post_id: @post.id, id: comment.id), method: :delete, data: {confirm: "コメントを削除してもよろしいですか?"} %>]</strong> </p> <% end %>
5. Postコントローラーを編集
app/controllers/posts_controller.rb
def show @comment = Comment.new @comments = @post.comments.includes(:user) end
6. ビューの微修正
コメント欄に投稿時刻等を記述
時刻の設定には、Railsのapplication.rbという設定ファイルを扱う
config/application.rb
class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 6.0 config.i18n.default_locale = :ja config.time_zone = 'Tokyo'
ja.ymlファイルを作成して、表示する時刻のフォーマットを設定
config/locales/ja.yml
ja: time: formats: default: "%Y/%m/%d %H:%M:%S"
この時刻設定を元に、lメソッドを利用して、表示する時刻へ反映
show.html.erb
<%= l comment.created_at %>
所感
もっとコードを簡潔にかけるかもしれないが、今の私にとってはこれが精一杯
次回
コメント機能の非同期化、もしくは住所登録/検索機能を実装する
ページネーション機能(kaminariの導入)
内容
コメント機能の前に、一覧ページでページネーション機能を実装する
ページネーション機能
1. kaminariの導入
railsでページネーションを実装するにはkaminariというgemが便利
gemfile
gem 'kaminari'
ターミナル
% bundle install % rails s
2. ビューでページネーションを表示
コントローラーを以下のように編集
posts_controller.rb
def index # ページネーションをつけたいデータに.page(params[:page])を追加 @posts = @q.result(distinct: true).includes(:user).page(params[:page]) end
続いてビューを以下のように編集
index.html.erb
# ページネーションを表示させたいところに<%= paginate @posts %>を追加 <%= paginate @posts %>
これで実装完了!簡単!
ページネーションが1ページに表示するレコード数はデフォルトで25件
よってレコード数が26件以上あれば、ページネーションが表示される
3. ページに表示するレコード数を変更したい時
1ページに表示するレコード数はデフォルトで25件のため、このレコード数を変更したい場合は以下のように記述
posts_controller.rb
# # .page(params[:page])の後に.per(10)を追加→数値によってレコード数を変更できる @posts = @q.result(distinct: true).includes(:user).page(params[:page]).per(10)
4. ページネーションのCSSを編集
ここではまずbootstrapを導入してからCSSを編集する
gemfile
gem 'bootstrap', '~> 5.1.0' # bootstrapはjQueryに依存するため以下も追加(既に導入してるので今回は飛ばす) gem 'jquery-rails'
sprockets-railsがv2.3.2.以上であるか確認してからインストール
% bundle show |fgrep sprockets-rails * sprockets-rails (3.2.2) % bundle install % rails s
その後、application.scssやapplication.jsを編集し、rails g kaminari:views bootstrap4(bootstrap5はなぜかなかった)を実行することでビューは編集できたのだが、ページネーション以外のビューが崩れてしまったため断念
bootstrapによるビューの修正は後回しとする
次回
投稿詳細ページでコメントのやり取りができるコメント機能の実装
参考
スクロール機能
サイドバーとメインコンテンツをdisplay: flex;で分けていて、別々にスクロールしたい場合、以下のように記述する
# 親要素にheightを指定 .contents { display: flex; height: 100vh; } # 子要素それぞれにoverflow-yを指定 .side-bar { overflow-y: auto; } .main { overflow-y: auto; }
これで別々にスクロールが可能となる
ソート機能(ransack)
内容
投稿の並び順を変えたいので、ransackを用いてソート機能を実装する
ソート機能
1. counter_cultureの導入
投稿をいいねの多い順に並べ替えたいため、postsテーブルにlikes_countというカラムを作りたい
Railsにはcounter_cacheという機能がデフォルトである
→すでにpostsテーブルにデータがある場合、likes_countの値は自動更新されない!
→counter_cultureはcounter_cacheの弱点も補強してくれるような高機能のgem、ということで導入
gemfile
gem 'counter_culture'
ターミナル
% bundle install % rails s
マイグレーションファイルを生成
ターミナル
% rails generate counter_culture Post likes_count
以下のようなマイグレーションファイルが生成される
db/migrate/2021xxxxxxxx_add_likes_count_to_posts.rb
class AddLikesCountToPosts < ActiveRecord::Migration[6.0] def self.up add_column :posts, :likes_count, :integer, null: false, default: 0 end def self.down remove_column :posts, :likes_count end end
ターミナル
% rails db:migrate
モデルには以下のように記載
model/like.rb
class Like < ApplicationRecord belongs_to :post counter_culture :post end
model/post.rb
class Post < ApplicationRecord has_many :likes, dependent: :destroy end
その後、すでにpostsテーブルにたくさんデータがある等の場合、コンソールで以下の様に実行
ターミナル
% rails c pry(main)> Like.counter_culture_fix_counts
こうすることで、postsテーブルのlikes_countのカラムの中身がアップデートされる
これは便利!
2. ビューの編集(ransack)
1.でいいねの数がpostsテーブルに取得できたので、ビューで人気順に並べ替えれるように編集
その他、投稿の新しい順や距離の長い順等も合わせて並べ替えができるように記述
_search.html.erb
<%= search_form_for @q, url: root_path, class: "search-form" do |f| %> <%= f.select(:sorts, {'投稿が新しい順': 'created_at desc', '投稿が古い順': 'created_at asc', '距離が長い順': 'distance desc', '距離が短い順': 'distance asc', '人気順': 'likes_count desc'}, { selected: params[:q][:sorts] }, class: "sort-input") %> <%= f.submit "検索", class: "search-btn" %> <% end %>
※前回の投稿では、検索後にsearch.html.erbに遷移するようにurl等も指定していたが、すべて投稿一覧ページで完結すると思いindex.html.erbに遷移するよう記述を変更した
3. 投稿一覧ページのビューを編集
サイドバー(検索機能)とメインコンテンツ(投稿一覧)に分けて表示
次回
他人の投稿にコメントできるようにコメント機能を実装
投稿検索機能(ransackの導入)
内容
コメント機能の前に、投稿検索機能を実装する
投稿検索機能
1. ransackを導入
ransackというgemを用いて検索機能を簡単に実装する
gemfile
gem 'ransack'
ターミナル
% bundle install % rails s
2. 検索機能のMVC設定
collectionとmemberを用いて、ルーティングを設定する
これを使用すると、生成されるルーティングのURLと実行されるコントローラーを任意にカスタムできる
collectionはルーティングに:idがつかない、memberは:idがつくという違いがある
routes.rb
resources :posts, except: :index do collection do get 'search' end end
コントローラーの編集
posts_controller.rb
before_action :set_search, only: %i[index search] private def set_search @q = Post.ransack(params[:q]) end
投稿一覧ページで検索ができるようにビューを編集(部分テンプレート)
_search.html.erb
<%= search_form_for @q, url: search_posts_path, class: "search-form" do |f| %> # gteqとlteqオプションを用いると、範囲を指定した検索が可能 <%= f.number_field :distance_gteq, placeholder: "例)5", class: "distance-search" %>km以上〜 <%= f.number_field :distance_lteq, placeholder: "例)10", class: "distance-search" %>km以下 # eq_anyオプションを用いると、check_boxのどれかに当てはまるものを検索可能 <% choices = ["多い", "やや多い", "どちらともいえない", "やや少ない", "少ない"] %> <% choices.each do |c| %> <%= f.check_box :traffic_eq_any, {multiple: true}, c, nil %> <%= f.label :traffic, c %><br> <% end %> # contを用いると、指定したカラムの単語の一部分を含むものを検索可能 <%= f.search_field :comment_cont, placeholder: "投稿を検索する", class: "search-input" %> <%= f.submit "検索", class: "search-btn" %> <% end %>
3. ソート機能のMVC設定
次回
ソート機能実装しようとしたが、いろんなとこで詰まってしまったため持ち越し
ransackを使った方がやりやすいのか使わない方がいいのか、そこも考えて取り組みたい
あと今回実装した検索機能で、検索したらsearch.html.erbをビューとして返すようにしたが、indexアクションいじってindex.html.erbを使い回した方がいいかと思った(その場合、上記の記述は変わる)
参考
ransackを用いた複数カラムのキーワード検索
複数のカラムでキーワード検索したい場合は、以下のように記述する
# orを用いてカラムを複数指定 <%= f.search_field :address_or_comment_cont, placeholder: "投稿を検索する", class: "search-input" %>
いいね機能
内容
いいね機能を投稿一覧表示と詳細表示ページにつける
いいね機能
Ajaxを用いて非同期通信でいいね機能を実装する
1. モデルの作成
Likeモデルを作成し、マイグレーションファイルを編集
ターミナル
% rails g model like
likes.rb
t.references :user, null: false, foreign_key: true t.references :post, null: false, foreign_key: true
ターミナル
% rails db:migrate
2. アソシエーションの記述
3. バリデーションの設定
1つの投稿にユーザーが1回しかいいねできないように、バリデーションを設定する
like.rb
validates :post_id, uniqueness: {scope: :user_id}
4. ルーティングの設定
投稿に紐づくようにルーティングをネストさせる
routes.rb
resources :posts, except: :index do resource :likes, only: %i[create destroy] end
5. コントローラーの設定
コントローラーを作成、createアクションとdestroyアクションを記述
ターミナル
% rails g controller likes
likes_controller.rb
before_action :post_find def create Like.create(user_id: current_user.id, post_id: @post.id) end def destroy @like = Like.find_by(user_id: current_user.id, post_id: @post.id) @like.destroy end private def post_find @post = Post.find(params[:post_id]) end
6. ビューの設定
Userモデルにメソッドを追加しておく
user.rb
def liked_by?(post_id) likes.where(post_id: post_id).exists? end
部分テンプレートを作成し、いいねボタンを押せるようにする
ユーザーがいいねボタンをすでに押しているかどうかで条件分岐
_like.html.erb
<% if user_signed_in? %> <% if current_user.liked_by?(post.id) %> <%= link_to post_likes_path(post.id), method: :delete, remote: true do %> <i class="fas fa-heart unlike-btn"></i> <% end %> <span><%= post.likes.count %></span> <% else %> <%= link_to post_likes_path(post.id), method: :post, remote: true do %> <i class="far fa-heart like-btn"></i> <% end %> <span><%= post.likes.count %></span> <% end %> <% else %> <i class="far fa-heart like-btn"></i><span><%= post.likes.count %></span> <% end %>
投稿一覧(および詳細)ページでは以下のように記述
このidはjsで操作する際に使用する
index.html.erb
<div id="likes-<%= post.id %>"> <%= render "shared/like", post: post %> </div>
7. Ajax(非同期通信)
まずは前提知識からまとめる
- romote: trueとは
一言で言うと、リクエストがhtml形式ではなくjs形式になる
- .js.erbファイルとは
一言で言えば、Rubyを埋め込むことのできるjsファイル
このファイル形式ではコントローラーで定義したインスタンス変数を記述することが可能となる
コントローラーは「app/views/コントローラ名/アクション名.リクエストの形式.〇〇」というファイルを探しに行くらしい
よって、remote: trueでリクエストの種類からレスポンスの種類までが異なることとなる
ファイルは自分でcreate.js.erbとdestroy.js.erbを作成(記述内容は両方とも同じ)
create.js.erb、destroy.js.erb
$('#likes-<%= @post.id %>').html("<%= j(render "shared/like", post: @post ) %>");
上記のjsではid名で操作するビューを取得しているが、このid名が、先ほどビューに入れたid名を取得している
※ちなみにrenderの前に記載の「j」は特殊文字をエスケープするためのメソッド
8. rubocop
次回
投稿詳細ページでコメント機能を実装
参考(今回の記事とは無関係)
1. 文字列を途中で区切って、「…」等と表記したい時
truncateを使用して文字列を切り捨てる
# 例)文字数30文字で切り捨てを行う場合 <%= truncate(post.comment, length: 30) %>
2. link_toでアラートを表示させたい時
data-confirmを使用する
# 例)投稿削除ボタンを押した時にアラートを表示したい時 <%= link_to "削除", post_path(post.id), method: :delete, data: {confirm: "投稿を削除してもよろしいですか?"} %>