[Ruby on Rails] How to implement tagging / incremental search function for posts (without gem)

Preface before tagging

Summary of this article

-Allow posts to be tagged. -Implement a function (incremental search) that automatically searches each time a character is entered in a tag.

Development environment

Mac OS Catalina 10.15.4 ruby 2.6 series rails 6.0 series

Completed image of tagging function

demo

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.

Flow of tagging function implementation

  1. Create Tag, Post, PostTagRelation, User model
  2. Edit migration files for various models
  3. Introduce Form object
  4. Routing settings
  5. Create posts controller, action definition
  6. Create view file
  7. Incremental search implementation (JavaScript)

Follow the steps above to implement.

1. Create Tag, Post, PostTagRelation, User model

er-figure

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. ..

2. Edit migration files for various models

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

3. Introduce Form object

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

4. Routing settings

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.

5. Create posts controller, action definition

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.

6. Create view file

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 ...

7. Implementation of Incremental Search (JavaScript)

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

[Ruby on Rails] How to implement tagging / incremental search function for posts (without gem)
Implement a refined search function for multiple models without Rails5 gem.
How to implement gem "summer note" in wysiwyg editor in Ruby on Rails
[For Rails beginners] Implemented multiple search function without Gem
Rails learning How to implement search function using ActiveModel
How to use Ruby on Rails
[Ruby on Rails] How to avoid creating unnecessary routes for devise
[Ruby on Rails] Search function (not selected)
How to implement search functionality in Rails
[Ruby on Rails] How to use CarrierWave
[Ruby on Rails] How to use kaminari
How to implement search function with rails (multiple columns are also supported)
How to build a Ruby on Rails environment using Docker (for Docker beginners)
[Ruby On Rails] How to search the contents of params using include?
Validation settings for Ruby on Rails login function
[Ruby on Rails] How to display error messages
How to add / remove Ruby on Rails columns
[Rails] How to implement unit tests for models
[For beginners] How to implement the delete function
[Ruby on Rails] How to install Bootstrap in Rails
[Ruby on Rails] How to use session method
How to implement Pagination in GraphQL (for ruby)
[Rails] Implement search function
[Ruby on Rails] Rails tutorial Chapter 14 Summary of how to implement the status feed
How to implement login request processing (Rails / for beginners)
[Ruby on Rails] How to write enum in Japanese
Rails / Ruby: How to get HTML text for Mail
[Ruby on Rails] How to change the column name
[Ruby on Rails] Implementation of tagging function/tag filtering function
[Ruby On Rails] How to reset DB in Heroku
(Ruby on Rails6) How to create models and tables
[Ruby on Rails] Search function (model, method selection formula)
Try to implement tagging function using rails and js
Explanation of Ruby on rails for beginners ④ ~ Naming convention and how to use form_Tag ~
[Rails] How to implement scraping
[Rails] Implementation of tagging function using intermediate table (without Gem)
[Ruby on Rails] Implement login function by add_token_to_users with API
How to display a graph in Ruby on Rails (LazyHighChart)
[Rails] How to display error messages for comment function (for beginners)
[Ruby On Rails] How to search and save the data of the parent table from the child table
[Ruby on Rails] Since I finished all Rails Tutorial, I tried to implement an additional "stock function"
[Ruby on Rails] Introduced paging function
I want to add a browsing function with ruby on rails
How to resolve OpenSSL :: SSL :: SSLError: SSL_connect on Ruby paypal-sdk-rest gem
[RSpec on Rails] How to write test code for beginners by beginners
How to deploy Bootstrap on Rails
Ruby on Rails for beginners! !! Post list / detailed display function summary
[Ruby on Rails] CSV output function
Rails on Tiles (how to write)
[Rails] How to put a crown mark on the ranking function
[Ruby on Rails] Comment function implementation
[Rails] How to implement star rating
[Ruby on Rails] "|| =" ← Summary of how to use this assignment operator
[Ruby on Rails] DM, chat function
(Ruby on Rails6) Create a function to edit the posted content
[Ruby on Rails] How to log in with only your name and password using the gem devise
[Ruby on Rails] When logging in for the first time ・ How to split the screen in half using jQuery
[Rails] Implementation procedure of the function to tag posts without gem + the function to narrow down and display posts by tags
How to create a query using variables in GraphQL [Using Ruby on Rails]
How to build a Ruby on Rails development environment with Docker (Rails 6.x)
Ruby on Rails DB Tips for creating methods to reduce the load