-Allow posts to be tagged. -Implement a function (incremental search) that automatically searches each time a character is entered in a tag.
Mac OS Catalina 10.15.4 ruby 2.6 series rails 6.0 series
Like Gif in the above figure, when you start entering tags, recommended tags can be displayed based on the tags saved in the DB. If you can implement the tagging function based on this article, I think that you can easily implement tag search etc.
Follow the steps above to implement.
First, let's introduce various models.
% rails g model tag
% rails g model post
% rails g model post_tag_relation
% rails g devise user
As it is, let's describe the validation by associating (associating) each introduced model.
post.rb
class Post < ApplicationRecord
has_many :post_tag_relations
has_many :tags, through: :post_tag_relations
belongs_to :user
end
tag.rb
class Tag < ApplicationRecord
has_many :post_tag_relations
has_many :posts, through: :post_tag_relations
validates :name, uniqueness: true
end
By setting "through :: intermediate table", the Post model and Tag model, which have a many-to-many relationship, are associated. As a caveat, it is necessary to link the intermediate table before referencing by through. (Since the code is read from the top, if you write in the order has_many: posts, through:: post_tag_relations → has_many: post_tag_relations, an error will occur.)
post_tag_relation
class PostTagRelation < ApplicationRecord
belongs_to :post
belongs_to :tag
end
user.rb
class User < ApplicationRecord
#<abridgement>
has_many :posts, dependent: :destroy
validates :name, presence: true
The has_many option of the User model is given dependent:: destroy so that when the parent element user information is deleted, that human post is also deleted.
In addition, the description (validates: 〇〇, presence: true) to prevent empty data from being saved in the Post model and Tag model is specified collectively in the form object to be created later, so it is not necessary now. ..
Next, add columns to the created model. (The minimum requirement is the tag name column, so arrange the others as you like.)
post migration file
class CreatePosts < ActiveRecord::Migration[6.0]
def change
create_table :posts do |t|
t.string :title, null: false
t.text :content, null: false
t.date :date
t.time :time_first
t.time :time_end
t.integer :people
t.references :user, foreign_key: true
t.timestamps
end
end
end
The reason we refer to user as a foreign key in the post migration file is to display the user name in the post list later.
tag migration file
class CreateTags < ActiveRecord::Migration[6.0]
def change
create_table :tags do |t|
t.string :name, null: false, uniqueness: true
t.timestamps
end
end
end
Applying uniqueness: true to the above name column is introduced to prevent duplicate tag names. (Since it is assumed that tags with the same name will be used many times, you may be wondering if it will not work as a tagging function if you prevent duplication, but how to reflect existing tags in posts will be described later. Will appear.)
post_tag_relation migration file
class CreatePostTagRelations < ActiveRecord::Migration[6.0]
def change
create_table :post_tag_relations do |t|
t.references :post, foreign_key: true
t.references :tag, foreign_key: true
t.timestamps
end
end
end
This post_tag_relation model plays the role of an intermediate table between the post model and the tag model, which has a many-to-many relationship.
user migration file
class DeviseCreateUsers < ActiveRecord::Migration[6.0]
def change
create_table :users do |t|
## Database authenticatable
t.string :name, null: false
t.string :email, null: false, default: ""
t.string :encrypted_password, null: false, default: ""
#<abridgement>
I wanted to use the username, so I added a name column.
Don't forget to execute the following command after editing the column.
% rails db:migrate
In this implementation, we want to save the input values from the post form to the posts table and tags table at the same time, so we will use the Form object.
First, create a forms directory in your app directory and create a posts_tag.rb file in it. Then, define a save method to save the values in the posts table and the tags table at the same time as shown below.
posts_tag.rb
class PostsTag
include ActiveModel::Model
attr_accessor :title, :content, :date, :time_first, :time_end, :people, :name, :user_id
with_options presence: true do
validates :title
validates :content
validates :name
end
def save
post = Post.create(title: title, content: content, date: date, time_first: time_first, time_end: time_end, people: people, user_id: user_id)
tag = Tag.where(name: name).first_or_initialize
tag.save
PostTagRelation.create(post_id: post.id, tag_id: tag.id)
end
end
Next, set the routing to run the index, new, and create actions of the posts controller.
routes.rb
resources :posts, only: [:index, :new, :create] do
collection do
get 'search'
end
end
The routing to the search action defined in the collection is used by the incremental search function.
Generate a controller in the terminal.
% rails g controller posts
The code in the generated posts controller file looks like this:
posts_controller.rb
class PostsController < ApplicationController
before_action :authenticate_user!, only: [:new]
def index
@posts = Post.all.order(created_at: :desc)
end
def new
@post = PostsTag.new
end
def create
@post = PostsTag.new(posts_params)
if @post.valid?
@post.save
return redirect_to posts_path
else
render :new
end
end
def search
return nil if params[:input] == ""
tag = Tag.where(['name LIKE ?', "%#{params[:input]}%"])
render json: {keyword: tag}
end
private
def posts_params
params.require(:post).permit(:title, :content, :date, :time_first, :time_end, :people, :name).merge(user_id: current_user.id)
end
end
In the create action, the value received by posts_params is saved in the Posts model and Tags table using the save method defined in the Form object earlier.
In the search action, based on the data acquired on the JS side (the character string typed in the tag input form), the data is pulled out from the tags table with the where + LIKE clause and returned to JS with reder json. (JS files will appear later.)
That's why the search action above isn't necessary if you don't implement incremental search.
new.html.erb
<%= form_with model: @post, url: posts_path, class: 'registration-main', local: true do |f| %>
<div class='form-wrap'>
<div class='form-header'>
<h2 class='form-header-text'>Timeline posting page</h2>
</div>
<%= render "devise/shared/error_messages", resource: @post %>
<div class="post-area">
<div class="form-text-area">
<label class="form-text">title</label><br>
<span class="indispensable">Mandatory</span>
</div>
<%= f.text_field :title, class:"post-box" %>
</div>
<div class="long-post-area">
<div class="form-text-area">
<label class="form-text">Overview</label>
<span class="indispensable">Mandatory</span>
</div>
<%= f.text_area :content, class:"input-text" %>
</div>
<div class="tag-area">
<div class="form-text-area">
<label class="form-text">tag</label>
<span class="indispensable">Mandatory</span>
</div>
<%= f.text_field :name, class: "text-box", autocomplete: 'off' %>
</div>
<div>[Recommended tag]</div>
<div id="search-result">
</div>
<div class="long-post-area">
<div class="form-text-area">
<label class="form-text">Event schedule</label>
<span class="optional">Any</span>
</div>
<div class="schedule-area">
<div class="date-area">
<label>date</label>
<%= f.date_field :date %>
</div>
<div class="time-area">
<label>Start time</label>
<%= f.time_field :time_first %>
<label class="end-time">End time</label>
<%= f.time_field :time_end %>
</div>
</div>
</div>
<div class="register-btn">
<%= f.submit "Post",class:"register-blue-btn" %>
</div>
</div>
<% end %>
Since the view file used in my application implementation is pasted solidly, the code is redundant, but the point is that there is no problem if the contents of the form can be sent to the routing by @post etc.
:index.html.erb
<div class="registration-main">
<div class="form-wrap">
<div class='form-header'>
<h2 class='form-header-text'>Timeline list page</h2>
</div>
<div class="register-btn">
<%= link_to "Move to the timeline posting page", new_post_path, class: :register_blue_btn %>
</div>
<% @posts.each do |post| %>
<div class="post-content">
<div class="post-headline">
<div class="post-title">
<span class="under-line"><%= post.title %></span>
</div>
<div class="more-list">
<%= link_to 'Edit', edit_post_path(post.id), class: "edit-btn" %>
<%= link_to 'Delete', post_path(post.id), method: :delete, class: "delete-btn" %>
</div>
</div>
<div class="post-text">
<p>■ Overview</p>
<%= post.content %>
</div>
<div class="post-detail">
<% if post.time_end != nil && post.time_first != nil %>
<p>■ Schedule</p>
<div class="post-date">
<%= post.date %>
<%= post.time_first.strftime("%H o'clock%M minutes") %> 〜
<%= post.time_end.strftime("%H o'clock%M minutes") %>
</div>
<% end %>
<div class="post-user-tag">
<div class="post-user">
<% if post.user_id != nil %>
■ Posted by: <%= link_to "#{post.user.name}", user_path(post.user_id), class:'user-name' %>
<% end %>
</div>
<div class="post-tag">
<% post.tags.each do |tag| %>
#<%= tag.name %>
<% end %>
</div>
</div>
</div>
</div>
<% end %>
</div>
</div>
This is also redundant, so please refer only where necessary ...
Here, I play with the JS file.
tag.js
if (location.pathname.match("posts/new")){
window.addEventListener("load", (e) => {
const inputElement = document.getElementById("post_name");
inputElement.addEventListener('keyup', (e) => {
const input = document.getElementById("post_name").value;
const xhr = new XMLHttpRequest();
xhr.open("GET", `search/?input=${input}`, true);
xhr.responseType = "json";
xhr.send();
xhr.onload = () => {
const tagName = xhr.response.keyword;
const searchResult = document.getElementById('search-result')
searchResult.innerHTML = ''
tagName.forEach(function(tag){
const parentsElement = document.createElement('div');
const childElement = document.createElement("div");
parentsElement.setAttribute('id', 'parents')
childElement.setAttribute('id', tag.id)
childElement.setAttribute('class', 'child')
parentsElement.appendChild(childElement)
childElement.innerHTML = tag.name
searchResult.appendChild(parentsElement)
const clickElement = document.getElementById(tag.id);
clickElement.addEventListener('click', () => {
document.getElementById("post_name").value = clickElement.textContent;
clickElement.remove();
})
})
}
});
})
};
I'm using location.pathname.match to load the code when the new action in the posts controller fires.
As a rough process in JS, ① Fire an event with keyup and send the input value of the tag form to the controller (around xhr. 〇〇) (2) Display the prediction tag on the front based on the information returned from the controller under xhr.onload. ③ When the prediction tag is clicked, that tag will be reflected in the form.
This completes the implementation of the tagging function and the implementation of incremental search. It's a rough article, but thank you for reading to the end!
Recommended Posts