概要
ポートフォリオにインスタグラムやTwitterで使用されているような
ハッシュタグもどきを実装しました。
今後実装される方の参考になればと思います。
参考サイト
Railsで作ったインスタもどきのキャプションにハッシュタグを実装
https://qiita.com/goppy001/items/791c946abdb41c9495bb
大まかな流れは上記サイトと同じですが、うまく出来なかったところを変更しています。
別の実装方法にて作り直したもの
本記事ではRails(フレームワーク)のヘルパーを多用していて、正直中身が見えない部分が多いと感じ、機能を作り直して実装方法を下記にまとめました。
下記の記事内では機能を細かく作成しているので、実装をする過程で学べることは多いように感じます。
https://qiita.com/Naoki1126/items/06e75badae93fc62d52d
#完成図
投稿画面
投稿詳細画面
ハッシュタグ一覧及びハッシュタグ投稿一覧画面
##事前準備DB
最初はbodyとuser_idのみのテーブル構成でしたが、ハッシュタグを入力するためのhashbodyカラムを追加しています。
ちなみに画像はポートフォリオの仕様上他テーブルへ保存しておりますが、imageのカラムがこのテーブル内にあっても問題はありません。
##モデル(DB)の作成
####ハッシュタグモデル
$rails g model Hashtag hashname:string
ハッシュタグ保存用のモデルを作成します。
hashnameカラムにハッシュタグが保存されます。
マイグレーションファイルの編集
class CreateHashtags < ActiveRecord::Migration[5.2]
def change
create_table :hashtags do |t|
t.string :hashname
t.timestamps
end
add_index :hashtags, :hashname, unique: true
end
end
中間テーブルの作成
$ rails g model HashtagPostImage post_image:references hashtag:references
HashtagテーブルとPostImage(投稿)テーブルの中間テーブルです。
参考にさせて頂いた記事ではここのコマンドが若干違います。
中間テーブルなので外部キーとしてhashtagとpostimageのidを持ってきます。
references型なので作成時にhashtag_idと打ってしまうと
出来上がったカラム名がhashtag_id_idとなってしまうので注意が必要です。
マイグレーションファイル
class CreateHashtagPostImages < ActiveRecord::Migration[5.2]
def change
create_table :hashtag_post_images do |t|
t.references :post_image, foreign_key: true
t.references :hashtag, foreign_key: true
end
end
end
マイグレート
$ rails db:migrate
##モデルのアソシエーションとバリデーションの設定
####ハッシュタグモデル
class Hashtag < ApplicationRecord
validates :hashname, presence: true, length: { maximum: 50 }
has_many :hashtag_post_images, dependent: :destroy
has_many :post_images, through: :hashtag_post_images
end
とりあえず50文字を上限にしました。
####中間テーブル
class HashtagPostImage < ApplicationRecord
belongs_to :post_image
belongs_to :hashtag
validates :post_image_id, presence: true
validates :hashtag_id, presence: true
end
####PostImageモデル
class PostImage < ApplicationRecord
has_many :hashtag_post_images, dependent: :destroy
has_many :hashtags, through: :hashtag_post_images
end
##PostImageモデルに下記を追加
after_create do
post_image = PostImage.find_by(id: id)
# hashbodyに打ち込まれたハッシュタグを検出
hashtags = hashbody.scan(/[##][\w\p{Han}ぁ-ヶヲ-゚ー]+/)
hashtags.uniq.map do |hashtag|
# ハッシュタグは先頭の#を外した上で保存
tag = Hashtag.find_or_create_by(hashname: hashtag.downcase.delete('#'))
post_image.hashtags << tag
end
end
#更新アクション
before_update do
post_image = PostImage.find_by(id: id)
post_image.hashtags.clear
hashtags = hashbody.scan(/[##][\w\p{Han}ぁ-ヶヲ-゚ー]+/)
hashtags.uniq.map do |hashtag|
tag = Hashtag.find_or_create_by(hashname: hashtag.downcase.delete('#'))
post_image.hashtags << tag
end
end
作成と更新時にこのアクションが実行されるように記入してあります。
・post_image = PostImage.find_by(id: id)
作成した投稿を探させます。
・ hashtags = hashbody.scan(/[##][\w\p{Han}ぁ-ヶヲ-゚ー]+/)
ここでは入力されたハッシュタグ、先頭に[##]がつく入力値を探し抽出します。
hashbodyは私のDBのカラム名ですので、ここはアプリケーションにより異なります。
投稿テーブルのテキスト入力用のカラムであれば何でも良いです。
・ hashtags.uniq.map do |hashtag|
# ハッシュタグは先頭の#を外した上で保存
tag = Hashtag.find_or_create_by(hashname: hashtag.downcase.delete('#'))
post_image.hashtags << tag
end
tagですが今回作成しようとしているHashtagが既に存在しているかを調べ、なければ作成します。(find_or_create_by)
downcase:大文字を小文字に変換。
<<:一つの投稿に対し複数のハッシュタグを一回で保存するために使っています。配列として追加するメソッド?です。
こうしてmapで繰り返すことにより、複数のハッシュタグがpostimageに保存されます。
・post_image.hashtags.clear
更新時、一回ハッシュタグを消しているようです。
##routeの記載
get '/post_image/hashtag/:name' => 'post_images#hashtag'
get '/post_image/hashtag' => 'post_images#hashtag'
私の場合はハッシュタグ一覧ページを作りたかったので二つのルートを用意しました。
##PostImage ヘルパーの編集
module PostImagesHelper
def render_with_hashtags(hashbody)
hashbody.gsub(/[##][\w\p{Han}ぁ-ヶヲ-゚ー]+/) { |word| link_to word, "/post_image/hashtag/#{word.delete("#")}",data: {"turbolinks" => false} }.html_safe
end
end
hashbodyの中より正規表現にヒットする情報を取り出しgsubで変換をする処理です。
link_to word, "/post_image/hashtag/#{word.delete("#")}"
ここのurlはアプリケーションの内容によって異なります。
ハッシュタグをクリックするとここのurlに飛びますという意味です。
先ほどrouteに書いたurlを打ち込みましょう。
##PostImagコントローラー
class PostImagesController < ApplicationController
def new
@postimagenew = PostImage.new
@postimagenew.post_image_images.new
end
def create
@postimagenew = PostImage.new(post_image_params)
@postimagenew.user_id = current_user.id
if @postimagenew.save
redirect_to post_images_path
else
render('post_images/new')
end
end
def destroy
@postimage = PostImage.find(params[:id])
@postimage.destroy
redirect_to post_images_path
end
private
def post_image_params
params.require(:post_image).permit(:body, :hashbody, :user_id, post_image_images_images: [], hashtag_ids: [])
end
ストロングパラメーター内の画像を他テーブルに配列で保存するためのpost_image_images_images:[]は気にしないでください。
hashtag_idsはハッシュタグをPostImageのcreate時に複数個登録するので記入してあります。
##View
###投稿フォーム
<div class= "row">
<div class="col-lg-2 col-md-2">
</div>
<div class="col-xs-12 col-lg-8 col-md-8 col-sm-12">
<div class= "postimage-new-box">
<% if @postimagenew.errors.any? %>
<div id="error_explanation">
<h3><%= @postimagenew.errors.count %>件の入力エラーにより、投稿出来ませんでした</h3>
<ul>
<% @postimagenew.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<h3>新規投稿</h3>
<div class="previw">
</div>
<%= form_with model:@postimagenew, local: true do |f| %>
<div class="attachment-field">
<p>画像を選択(複数指定可)</p>
<%= f.attachment_field :post_image_images_images, multiple:true %>
</div>
<br>
<br>
<div class= "postimage-body-box">
<p>投稿の詳細を入力してください</p>
<%= f.text_area :body, size:"55x12" %>
<br>
<p>ハッシュタグ入力欄</p>
<%= f.text_area :hashbody, size:"55x3" %>
<br>
<div class= "postimage-new">
<%= f.submit "新規投稿" ,class:'postimage-new-button' %>
</div>
<% end %>
</div>
</div>
</div>
<div class="col-lg-2 col-md-2">
</div>
</div>
Viewにて以下を表記します。
<%= render_with_hashtags(@postimage.hashbody) %>
上記は先ほどhelperで作成したメソッドを呼び出しています。
ちなみにpost_images/showのコントローラーはこちらです。
def show
@postimage = PostImage.find(params[:id])
end
単純に@postimageのhashtag入力欄の情報をヘルバーに渡しているんではないかと考えています。
###PostImageコントローラーにhashtagアクションの追加
def hashtag
@user = current_user
if params[:name].nil?
@hashtags = Hashtag.all.to_a.group_by{ |hashtag| hashtag.post_images.count}
else
@hashtag = Hashtag.find_by(hashname: params[:name])
@postimage = @hashtag.post_images.page(params[:page]).per(20).reverse_order
@hashtags = Hashtag.all.to_a.group_by{ |hashtag| hashtag.post_images.count}
end
end
ここの表記は作成するサイトにより変わります。
私はハッシュタグ一覧がみれるページを作りたかったので、params[:name].nil?の場合は
post_imageを表示しないという条件分岐をしています。
またgroup_byですが、hashtagに紐づく投稿が多い順番でハッシュタグを表示できるように
このような表記をしています。
##ハッシュタグのView
<div class="row">
<% if params[:name] == nil %>
<% else %>
<div class= "col-xs-12 col-lg-12 col-md-12 col-sm-12">
<div class="hashtag-post-box">
<h3 class="search-title">#<%= @hashtag.hashname %>: <%= @postimage.count %> 件 </h3>
<div class="flex-box">
<% @postimage.each do |postimage| %>
<div class= "post-image-index-post-box">
<p class="index-post-box-top">
<%= postimage.created_at.strftime("%Y/%m/%d") %>
</p>
<span class='far fa-comments index-comment-count' id='comment-count_<%= postimage.id %>' style="color: #777777;">
<%= render 'post_image_comments/comment-count', postimage:postimage %>
</span>
<span id = "favorite-button_<%= postimage.id %>"class="post-box-top-favorite">
<%= render 'post_image_favorites/favorite',postimage: postimage %>
</span>
<%= link_to post_image_path(postimage),data: {"turbolinks" => false} do %>
<ul class="slider">
<% postimage.post_image_images.each do |post| %>
<li>
<%= attachment_image_tag post, :image ,size:'430x360', format:'jpg',class:"image" %>
</li>
<% end %>
</ul>
<% end %>
<p class="hashtag-post-box-name">
<%= link_to user_path(postimage.user) do %>
<%= attachment_image_tag postimage.user, :profile_image,size:'30x30', format:'jpg',fallback:'no_image.jpg',class:'min-image' %>
<span class="index-post-box-user"><%= postimage.user.name %>
</span>
<% end %>
</p>
<div class="image-show-body-hash" style="padding:2%">
<%= simple_format(postimage.body.truncate(50))%>
<% if postimage.body.length > 50 %>
<span class="text-prev"><%= link_to '続きを読む', post_image_path(postimage), data: {"turbolinks" => false} %>
</span>
<% end %>
</div>
</div>
<% end %>
</div>
</div>
<div class="image-index-pagination" data-turbolinks="false">
<%= paginate @postimage,class:"paginate" %>
</div>
</div>
<% end %>
</div>
<div class="row">
<div class= "col-xs-12 col-lg-12 col-md-12 col-sm-12">
<div class= "hashtag-name">
<% @hashtags.sort.reverse.each do |count| %>
<% count[1].each do |hashtag| %>
<p><%= link_to "##{hashtag.hashname} (#{hashtag.post_images.count}) 件","/post_image/hashtag/#{hashtag.hashname}",data: {"turbolinks" => false} %>
</p>
<% end %>
<% end %>
</div>
</div>
</div>
</div>
クラス名とかを使いまわしていて訳わかんなくなっていてすいません。
大事なところは以下です。
<% if params[:name] == nil %>
<% else %>
<% end %>
この表記で先ほどrouteに書いたpost_image/hashtagとpost_image/hashtag/:nameでの条件分岐をしています。
paramsがnilのときの処理をコントローラーとViewそれぞれに書くことでエラーを起こさせないようにしています。
<div class= "hashtag-name">
<% @hashtags.sort.reverse.each do |count| %>
<% count[1].each do |hashtag| %>
<p><%= link_to "##{hashtag.hashname} (#{hashtag.post_images.count}) 件","/post_image/hashtag/#{hashtag.hashname}",data: {"turbolinks" => false} %>
</p>
<% end %>
<% end %>
</div>
ここにハッシュタグ一覧を表示しています。
表示はハッシュタグに紐づく投稿が多い順に表示しています。
####まとめ
ハッシュタグ入力欄を別にしないと、投稿の説明文のところにそのまま文章としてハッシュタグが残ってしまうため別カラムへの保存という形で実装しました。
投稿と一緒にハッシュタグを表示させている箇所では、意図的にhashbodyを非表示にしています。
view上にやたらと存在するturbolinks falseですがjsがうまく動かなくて書いてあるものなので、無視して頂いて大丈夫です。
初めての投稿で分かりづらい箇所があれば申し訳ないです。
これからポートフォリオを作成する方の参考になれば幸いです。
###追記
2020/7/1
中間テーブルのマイグレーションファイルを修正しました。
id Falseの設定をかけるとdestroy出来なくなる現象がありましたので、修正しております。
同時にモデルのhas_manyに対しdependent: :destroyを追加しまいました。
また、ハッシュタグと投稿の保存の部分に関してコントローラーとViewを追加しました。
2020/9/17
今回、投稿と別にハッシュタグの入力欄を設けましたが、
本来であればテキスト入力欄に一緒に入力し、その中でハッシュタグを識別させる方がスマートだと思います。
それ自体はモデルでscanする対象をテキスト入力欄に指定すれば可能なのですが、
表示の時にリンクになっていないハッシュタグも一緒に出てきてしまうという問題がありました。
(重複する)
表示の際に例えば、テキスト入力欄の#から始まる部分をモデルでの処理同様にscan→deleteしてみたりすれば入力欄をわざわざ分けなくても作成できるかもしれません。
scan、delete、match、include、exclude、presentあたりを用いて上手く活用出来れば恐らくできると思います。RubyやRailsにはオブジェクトの中身を識別して操作をするためのメソッドがたくさん用意されているので、是非調べてみてください。