__-Implement a function that allows you to tag your own posts. __ __ ・ Implement a function that allows you to narrow down the search using the tags attached to the post. __
·environment Ruby 2.6 series Rails 5.2 series
·Library Slim
__ ・ Rails application template of the above environment __ Procedure to set up Rails application and install devise and Slim
__ ↓ Completed image ↓ __
-User: Post
= User has many Posts for each person, so there is a `1 to many`
relationship.
- Post: Tag
= One Post has many tags` ``, `` `One Tag also has many Posts` `` Many-to-many
relationship.
→ Many-to-many relationship requires an intermediate table = TagMap
→ An intermediate table is a table that stores only the foreign keys (post_id and tag_id) of the many-to-many table (post and tag table in this case), and manages each other's tables with only the foreign keys. Make it possible.
$ rails g devise User //Create User model from devise $ rails g migration add_columns_to_users name:string //name column added $ rails g model Post content:string user:references //Post model creation $ rails g model Tag tag_name:string //Tag model creation $ rails g model TagMap post:references tag:references //TagMap model creation
>> -Since columns cannot be added to the User model created by devise, when adding the name column, it is necessary to add it as a new migration.
-<Table name>: A foreign key is set for the table specified in references. (Required)
### 3. Check the created migration file
> #### 3-1. users migration file
>>```db/migrate/[timestamps]_create_devise_users.rb
class DeviseCreateUsers < ActiveRecord::Migration[5.2]
def change
create_table :users do |t|
## Database authenticatable
t.string :email, null: false, default: ""
t.string :encrypted_password, null: false, default: ""
#<abridgement>
end
end
3-2. Add name column to users table
class AddUsernameToUsers < ActiveRecord::Migration[5.2] def change add_column :users, :name, :string end end
> #### 3-3.posts migration file
>>```ruby:db/migrate/[timestamps]_create_posts.rb
class CreatePosts < ActiveRecord::Migration[5.2]
def change
create_table :posts do |t|
t.text :content
t.references :user, foreign_key: true
t.timestamps
end
end
end
3-4.tags migration file
class CreatePostTags < ActiveRecord::Migration[5.2] def change create_table :post_tags do |t| t.string :tag_name t.timestamps end end end
> #### 3-5.tag_maps migration file
>>```ruby:db/migrate/[timestamps]_create_tag_maps.rb
class CreatePostTagMaps < ActiveRecord::Migration[5.2]
def change
create_table :tag_maps do |t|
t.references :post, foreign_key: true
t.references :tag, foreign_key: true
t.timestamps
end
end
end
4-1. User model file
class User < ApplicationRecord
devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable has_many :posts, dependent: :destroy end
>> ・ `dependent:: destroy`: An option that can be attached to the parent class so that the child class can be deleted when the parent class is deleted.
> #### 4-2. Post model file
>>```ruby:app/models/user.rb
class Post < ApplicationRecord
belongs_to :user
has_many :tag_maps, dependent: :destroy
has_many :tags, through: :tag_maps
end
__ ・ The
through option is used to associate with the tags table through the tag_maps table. By doing this, you can get the tags associated with Post by using
Post.tags`. This can be used when you want to get the tag attached to the post and display it on the post details screen. __
__ ・ When using the through option, it is necessary to associate it with the intermediate table first. __
__-By adding the dependent :: destroy option to the intermediate table, the relationship between Post and Tag will be deleted at the same time as Post is deleted. __
4-4.Tag model file
class Tag < ApplicationRecord has_many :tag_maps, dependent: :destroy, foreign_key: 'tag_id' has_many :posts, through: :tag_maps end
>> __` ・ After associating with the tag_maps table, it is associated with the posts table through tag_maps`. If you use `Tag.posts`, you can get the Posts associated with the tags. This can be used when you want to search for posts with a specific tag, such as "sports". __
> #### 4-5. TagMap model file
>>```ruby:app/models/tag_map.rub
class PostTagMap < ApplicationRecord
belongs_to :post
belongs_to :tag
validates :post_id, presence: true
validates :tag_id, presence: true
end
__ ・ Since it is owned by multiple Posts and multiple Tags, it is associated with belongs_to. __
__ ・ When building the relationship between Post and Tag, it is absolute that there are two foreign keys, so validate it. __
Rails.application.routes.draw do devise_for :users root to: 'posts#index' resources :posts end
>> __ ・ At this point, for the time being, set the routing provided by devise, the root path, and the path to the posts resource. __
## First, implement from the function that can tag posts
### 6. Create a controller
> #### 6-1. Write the code to create posts and tags in the create action of the posts controller.
>>```ruby:app/controllers/posts_controller.rb
class PostsController < ApplicationController
def create
@post = current_user.posts.new(post_params)
tag_list = params[:post][:tag_name].split(nil)
if @post.save
@post.save_tag(tag_list)
redirect_back(fallback_location: root_path)
else
redirect_back(fallback_location: root_path)
end
end
private
def post_params
params.require(:post).permit(:content)
end
end
__ Let's break down the above code. __
@post = current_user.posts.new(post_params)
>> __ ・ By setting `current_user.posts`, the id of the logged-in user will be saved in the user_id of the posts table, but if ʻUser and Post are not associated, an error will occur. Masu`. Strong Parameters is used as an argument. __
>>```ruby:Get the tags that have been sent
tag_list = params[:post][:tag_name].split(nil)
__ ・
params [: post] [: tag_name]
: From form, refer to @post object and send the tag name together, so get it in this form. For example, it is sent like "" sports "" study "" work "". __
__ ・
.split (nil)
: Arrange the sent values separated by spaces. In the above example, it feels like ["sports", "study", "work"]. The reason for arranging is that it is necessary to retrieve this value one by one by iterative processing when saving it in the database later. __
@post.save_tag(tag_list)
__ ・ `Process to save the array of tags acquired earlier in the database`. The definition of save_tag that performs the processing will be described later. __
> #### 6-2. Write the code to get the post and tag in the index action.
>>```ruby:app/controllers/posts_controller.rb
def index
@tag_list = Tag.all #Get all to display the tag list in the view.
@posts = Post.all #Get all to display post list in view.
@post = current_user.posts.new #View form_Used for model with.
end
def show @post = Post.find(params[:id]) #Get the post you clicked. @post_tags = @post.tags #Get the tag associated with the clicked post. end
### 7. Define the save_tag instance method in the Post model file.
> __ ・ Define the contents of the save_tag instance method described earlier in the create action. __
>```ruby:app/models/post.rb
class Post < ApplicationRecord
#<abridgement>
def save_tag(sent_tags)
current_tags = self.tags.pluck(:tag_name) unless self.tags.nil?
old_tags = current_tags - sent_tags
new_tags = sent_tags - current_tags
old_tags.each do |old|
self.post_tags.delete PostTag.find_by(post_tag_name: old)
end
new_tags.each do |new|
new_post_tag = PostTag.find_or_create_by(post_tag_name: new)
self.post_tags << new_post_tag
end
end
end
current_tags = self.tags.pluck(:tag_name) unless self.tags.nil?
> __ ・ If there is a tag associated with @post saved by the create action earlier, get all "tag names as an array". __
>```ruby:Get old tags
old_tags = current_tags - sent_tags
__ ・ Old_tags are the tags that exist in the currently acquired @post, excluding the sent tags. __
new_tags = sent_tags - current_tags
> __ ・ New_tags are the tags that are sent, excluding the tags that currently exist. __
>```ruby:Delete old tags
old_tags.each do |old|
self.tags.delete Tag.find_by(tag_name: old)
end
__-Delete old tags. __
new_tags.each do |new| new_post_tag = Tag.find_or_create_by(tag_name: new) self.post_tags << new_post_tag end
> __-Save the new tag in the database. __
> __ ・ The reason why it takes time to get old tags and new tags one by one and delete them as described above is not only to get and save tags one by one, for example, when editing a post. Because it needs to work.
However, I'm sorry, I will omit the implementation of the editing function this time. __
### 8. Create a view.
> #### 8-1. Create a view of the post list.
>>```ruby:app/views/posts/index.html.slim
h3 tag list
- @tag_list.each do |list|
span
= link_to list.tag_name, tag_posts_path(tag_id: list.id)
= "(#{list.posts.count})"
hr
h3 post
= form_with(model: @post, url: posts_path, local: true) do |f|
= f.text_area :content
br
= "You can add multiple tags by entering a space."
= "Example: Music, Literature, Sports"
= f.text_field :tag_name
br
= f.submit
hr
h3 post list
- @posts.each do |post|
= link_to post.content, post
Let's break down the above code.
>> __ ・ All the tags acquired by the index action are displayed, and `a link to the path to display posts related to that tag is provided`. Click this link to view the posts associated with that `tag`. The routing settings to get this link and the creation of actions will be described later. __
>> __ ・ `" (# {list.posts.count}) "` counts and displays how many posts currently have that tag. __
>>```ruby:New post form (can be tagged)
= form_with(model: @post, url: posts_path, local: true) do |f|
= f.text_area :content
br
= "You can add multiple tags by entering a space."
= "Example: Music, Literature, Sports"
= f.text_field :tag_name
br
= f.submit
__ ・
= f.text_field: tag_name
: By writing this line, the tag entered in the form will be sent to the create action as a parameter called params [: post] [: tag_name]. .. __
8-2. Create a view of the post detail page.
h1 post details p= @post.content br = "tag: "
>> __ ・ The part that displays the tag is the same as the method described in the list display. In this case, the part called `tag associated with a specific post` is different. __
__ · This completes the implementation of the function that allows you to tag posts. __
## Implement the function to narrow down and display posts by tags
### 1. Add routing.
>```ruby:config/routes.rb
Rails.application.routes.draw do
devise_for :users
root to: 'posts#index'
resources :posts
#Routing to actions to view posts filtered by tags
resources :tags do
get 'posts', to: 'posts#search'
end
end
__ ・ By nesting, you can use the path to transition to the post page associated with a specific tag called
tag_posts_path (tag_id: tag.id)
as described earlier. __
def search @tag_list = Tag.all #Get all tags to display all tags on this post list display page @tag = Tag.find(params[:tag_id]) #Get the clicked tag @posts = @tag.posts.all #Show all posts associated with the clicked tag end
### 3. Create a page view that displays a list of posts narrowed down by tags.
> __ ・ First, create a search.html.slim file in the app / views / posts directory. __
>```ruby:app/views/posts/search.html.slim
h2 post list
#Tag list
- @tag_list.each do |list|
span
= link_to list.tag_name, tag_posts_path(post_tag_id: list.id)
= "(#{list.posts.count})"
br
#Post list narrowed down by tag
= "The tag is ─"
strong= "#{@tag.tag_name}"
= "─ Post list"
br
- @posts.each do |post|
= link_to post.content, post
br
= link_to 'home', root_path
__ ・ The tag list part is the same as the index part. __
= "The tag is ─" strong= "#{@tag.tag_name}" = "─ Post list"
> __ ・ In this part, the relevant tag is displayed so that you can see what kind of tag was narrowed down. __
__ ・ This completes the implementation of the function to display posts narrowed down by tags. As you can see, there are some overlaps in the view, so if you partial and refactor those parts, the view will look cleaner and look better. __
## Article that was very helpful
[Note when implementing the tag function in Rails without using gem](https://qiita.com/tobita0000/items/daaf015fb98fb918b6b8)
Recommended Posts