-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