This time, I would like to implement the function that allows you to enter tags like this and the function that predictive conversion is displayed below each time you enter a tag!
In the image, if you type "acid" in the tag input form, "sourness" is displayed below as a predictive conversion. However, since the browser is smart, the browser also outputs predictive conversion, but ... lol
Terminal
% cd ~/projects
% rails _6.0.0_ new tagtweet -d mysql
% cd tagtweet
Before creating the database, let's change the encoding setting described in database.yml.
config/database.yml
default: &default
adapter: mysql2
# encoding: utf8mb4
encoding: utf8
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
username: root
password:
socket: /tmp/mysql.sock
Then create a database
Terminal
rails db:create
so
Created database 'tagtweet_development' Created database 'tagtweet_test'
Is created
Since tweet and tag have a many-to-many relationship, Of the intermediate table The point is to create a ** tweet_tag_relations table **
Terminal
% rails g model tweet
% rails g model tag
% rails g model tweet_tag_relation
db/migrate/20XXXXXXXXXXXX_create_tweets.rb
class CreateTweets < ActiveRecord::Migration[6.0]
def change
create_table :tweets do |t|
t.string :message, null:false
#Added messege column
t.timestamps
end
end
end
db/migrate/20XXXXXXXXXXXX_create_tags.rb
class CreateTags < ActiveRecord::Migration[6.0]
def change
create_table :tags do |t|
t.string :name, null:false, uniqueness: true
#Add name column
t.timestamps
end
end
end
This time, we'll set a constraint of "uniqueness: true" to avoid duplicate tag names.
db/migrate/20XXXXXXXXXXXX_create_tweet_tag_relations.rb
class CreateTweetTagRelations < ActiveRecord::Migration[6.0]
def change
create_table :tweet_tag_relations do |t|
t.references :tweet, foreign_key: true
t.references :tag, foreign_key: true
t.timestamps
end
end
end
In the tweet_tag_relations table, the information in the "tweets table" and "tags table" is referenced, so "foreign_key: true" is used.
Terminal
rails db:migrate
tweet.rb
class Tweet < ApplicationRecord
has_many :tweet_tag_relations
has_many :tags, through: :tweet_tag_relations
end
tag.rb
class Tag < ApplicationRecord
has_many :tweet_tag_relations
has_many :tweets, through: :tweet_tag_relations
end
tweet_tag_relation.rb
class TweetTagRelation < ApplicationRecord
belongs_to :tweet
belongs_to :tag
end
routes.rb
Rails.application.routes.draw do
root to: 'tweets#index'
resources :tweets, only: [:new, :create]
end
When you tweet something, we aim to have a specification in which "tweet" and "tag" are saved at the same time. Form objects are useful for such implementations.
Form objects are tools used to update multiple models in a single form submission. You can treat your own class like a model. This Form object can be used by loading a module called "ActiveModel :: Model".
ActiveModel::Model
"ActiveModel :: Model" is a tool that allows you to use helper methods such as "form_for" and "render" as in ActiveRecord. You will also be able to use the "Model Name Survey" and "Validation" functions.
First, let's create tweets_tag.rb in the models directory
The layout is app/models/tweets_tag.rb.
tweets_tag.rb
class TweetsTag
include ActiveModel::Model
# include ActiveModel::Create a From object by describing a Model
attr_accessor :message, :name
#Create virtual attributes that can be both getter and setter roles
# :First of all, it is ok to understand that you can save it by writing the column you want to save such as name
with_options presence: true do
validates :message
validates :name
end
def save
tweet = Tweet.create(message: message)
tag = Tag.create(name: name)
TweetTagRelation.create(tweet_id: tweet.id, tag_id: tag.id)
end
#Describe the process to save the value in the case table in the save method
end
Since the uniqueness constraint needs to be set for each model, let's describe it in the tag model.
tag.rb
class Tag < ApplicationRecord
has_many :tweet_tag_relations
has_many :tweets, through: :tweet_tag_relations
validates :name, uniqueness: true
end
Terminal
% rails g controller tweets
tweets_controller.rb
class TweetsController < ApplicationController
def index
@tweets = Tweet.all.order(created_at: :desc)
end
def new
@tweet = TweetsTag.new
end
def create
@tweet = TweetsTag.new(tweet_params)
if @tweet.valid?
@tweet.save
return redirect_to root_path
else
render "new"
end
end
private
def tweet_params
params.require(:tweets_tag).permit(:message, :name)
end
end
I am using the new method for the "Form object".
I am using the save method defined in the From object
tweets/index.html.erb
<div class="header">
<div class="inner-header">
<h1 class="title">
TagTweet
</h1>
<li class='new-post'>
<%= link_to "New Post", new_tweet_path, class:"post-btn"%>
</li>
</div>
</div>
<div class="main">
<div class="message-wrap">
<% @tweets.each do |tweet|%>
<div class="message">
<p class="text">
<%= tweet.message %>
</p>
<ul class="tag">
<li class="tag-list">
<%tweet.tags.each do |tag| %>
#<%=tag.name%>
<%end%>
</li>
</ul>
</div>
<%end%>
</div>
</div>
tweets/new.html.erb
<%= form_with model: @tweet, url: tweets_path, class:'form-wrap', local: true do |f| %>
<div class='message-form'>
<div class="message-field">
<%= f.label :message, "Tweet" %>
<%= f.text_area :message, class:"input-message" %>
</div>
<div class="tag-field", id='tag-field'>
<%= f.label :name, "tag" %>
<%= f.text_field :name, class:"input-tag" %>
</div>
<div id="search-result">
</div>
</div>
<div class="submit-post">
<%= f.submit "Send", class: "submit-btn" %>
</div>
<% end %>
CSS omitted! !! !!
tweets_tag.rb
class TweetsTag
include ActiveModel::Model
attr_accessor :message, :name
with_options presence: true do
validates :message
validates :name
end
def save
tweet = Tweet.create(message: message)
tag = Tag.where(name: name).first_or_initialize
tag.save
TweetTagRelation.create(tweet_id: tweet.id, tag_id: tag.id)
end
end
tag = Tag.where(name: name).first_or_initialize
I will explain
Use the first_or_initialize method with the where method. where method By specifying the condition in the argument part like model .where (condition), you can get the "instance of the record that matches the condition" in the form of an array. Be sure to include the "column to be searched" in the argument condition and describe the conditional expression. If there is a record of the condition searched by where, an instance of that record is returned, otherwise a new instance is added. It is a method to make
** For now, the implementation of tagged tweets is complete ** ** If you want to tag tags that have already been saved in the database, it will be a more convenient application if there is a search function that can display tags that match the input characters as candidates during input **
With the sequential search function, if the tag "rails" already exists in the database, when the character "r" is entered, "rails" that matches the character "r" is displayed on the screen in real time as a candidate. A common guy to do When implementing by programming, it seems to be called ** incremental search **
Let's say that we should implement it,
application.js
require("@rails/ujs").start()
// require("turbolinks").start() //Comment out this line
require("@rails/activestorage").start()
require("channels")
If you do not comment out the above line, the event set in js may not fire, so it is safe to comment out
tweets_controller
class TweetsController < ApplicationController
#abridgement
def search
return nil if params[:keyword] == ""
tag = Tag.where(['name LIKE ?', "%#{params[:keyword]}%"] )
render json:{ keyword: tag }
end
And define search actions
The LIKE clause is used when searching for ambiguous strings and is used with the where method.
% Contains any string, including blank strings
In short, based on the value received in params [: keyword], either the condition matches in the name column, or the one searched in the tag table is assigned to tag.
It is in json format, keyword is the key, tag is the value, and the result is returned to js.
routes.rb
Rails.application.routes.draw do
root to: 'tweets#index'
resources :tweets, only: [:index, :new, :create] do
collection do
get 'search'
end
end
end
Nesting (nesting) routing allows this parent-child relationship to be represented by routing.
collections and members can be used when configuring routing. It allows you to optionally customize the URL of the generated routing and the controller to be executed.
The difference is that collection does not have: id in the routing, and member has: id.
In the case of this search function, it is not necessary to specify the: id like the detail page to go to a specific page, so let's set the routing using collection
How to create tag.js for app/javascript/packs
application.js
Let's edit as follows to load tag.js
require("@rails/ujs").start()
// require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")
require("./tag")
tag.js
if (location.pathname.match("tweets/new")){
document.addEventListener("DOMContentLoaded", () => {
console.log("Loading completed");
});
};
location.pathname gets the URL of the current page, .match returns the matching result of the string passed as an argument In other words, the event fires when you are currently in tweets/new!
document is the whole html element addEventListener executes various event processing
DOMContentLoaded is when the web page has finished loading
That is, execute the event when the entire html element is loaded
OK when "Loading completed" is displayed on the console
tag.js
if (location.pathname.match("tweets/new")){
document.addEventListener("DOMContentLoaded", () => {
const inputElement = document.getElementById("tweets_tag_name");
inputElement.addEventListener("keyup", () => {
const keyword = document.getElementById("tweets_tag_name").value;
});
});
};
Get the html element with id tweets_tag_name and assign it to InputElement
** Be careful here! !! ** **
Was there an element with the id that was tweets_tag_name? ??
tweets/new.html.erb
<%= form_with model: @tweet, url: tweets_path, class:'form-wrap', local: true do |f| %>
<div class='message-form'>
<div class="message-field">
<%= f.label :message, "Tweet" %>
<%= f.text_area :message, class:"input-message" %>
</div>
<div class="tag-field", id='tag-field'>
<%= f.label :name, "tag" %>
<%= f.text_field :name, class:"input-tag" %>
</div>
<div id="search-result">
</div>
</div>
<div class="submit-post">
<%= f.submit "Send", class: "submit-btn" %>
</div>
<% end %>
Also, index.html.erb doesn't have such an id. .. .. .. ..
But why can I get it? From the conclusion
** Because form_with will give you an id without permission **
More specifically, for example
form_with model: @tweet
tweets_controller
so
def new
@tweet = TweetsTag.new
end
Is defined as
First, the id becomes ** tweet_tag **
And
** drinks/new.html.erb **
<%= f.label :name, "tag" %>
<%= f.text_field :name, class:"input-tag" %>
: name is
Attach to ** tweet_tag **, ** tweet_tag_name **
The id is generated! !!
The opinion is reasonable, "I can't believe who said where!" Check with the validation tool to see if form_with actually generated the id
In the place of the message to tweet the tweet
An id called ** tweets_tag_messages ** was generated, and that was <%= f.text_area :message, class:"input-message" %>
Is given to.
Where to tag
An id called ** tweets_tag_name ** is generated, which is
<%= f.text_field :name, class:"input-tag" %>
Is given to.
** id is given by form_with! !! !! ** **
Please keep that in mind
Now you have the input form
app/javascript/packs/tag.js
if (location.pathname.match("tweets/new")){
document.addEventListener("DOMContentLoaded", () => {
const inputElement = document.getElementById("tweets_tag_name");
// form_Get the input form itself based on the id generated by with
inputElement.addEventListener("keyup", () => {
//Event fire when keyboard key is released from input form
const keyword = document.getElementById("tweets_tag_name").value;
// .By setting value, the value entered in the input form can be retrieved.
//Get the value actually entered and enter it in keyword
console.log(keyword);
});
});
};
At this point, let's fill out the form. It is ok if the entered characters can be output to the console.
packs/tag.js
if (location.pathname.match("tweets/new")){
document.addEventListener("DOMContentLoaded", () => {
const inputElement = document.getElementById("tweets_tag_name");
inputElement.addEventListener("keyup", () => {
const keyword = document.getElementById("tweets_tag_name").value;
const XHR = new XMLHttpRequest();
})
});
};
const XHR = new XMLHttpRequest (); Create an instance using the XMLHttpRequest object and assign it to the variable XHR Let's create the XMLHttpRequest object needed for asynchronous communication. You can send any HTTP request by using the XMLHttpRequest object.
tag.js
if (location.pathname.match("tweets/new")){
document.addEventListener("DOMContentLoaded", () => {
const inputElement = document.getElementById("tweets_tag_name");
inputElement.addEventListener("keyup", () => {
const keyword = document.getElementById("tweets_tag_name").value;
const XHR = new XMLHttpRequest();
XHR.open("GET", `search/?keyword=${keyword}`, true);
XHR.responseType = "json";
XHR.send();
})
});
};
XHR.open("GET", `search/?keyword=${keyword}`, true);
Specify the HTTP method as the first argument of the open method, the URL as the second argument, and true as the third argument to indicate that it is asynchronous communication.
The reason why such a URL is specified is,
This URL is called the query parameter, like http://sample.jp/?name=tanaka,
A URL parameter that spells information after the "?".
The structure after "?" Is **?
This time, it is not necessary to identify tweets by: id, so specify the query parameter.
** Why is search omitted in the URL when I want to run drinks # search **
Because the path can be specified relatively based on the directory one level above the specified path.
For example, the path specified this time is search/keyword = hogehoge And the directory above is tweets, so It seems that it will complement the directory one level higher without permission. .. .. ..
Now you can run Drinks # search
I thought,
XHR.responseType = "json";
Let's specify the json format as the format of the data returned from the controller by writing
And finally!
XHR.send();
Let's write and send a request.
Every time something is entered in the tag input form, the rails search action moves!
Let's receive the data returned as a response when the server-side processing is successful. Use the response property to receive the data.
tag.js
if (location.pathname.match("tweets/new")){
document.addEventListener("DOMContentLoaded", () => {
#abridgement
XHR.send();
XHR.onload = () => {
const tagName = XHR.response.keyword;
};
});
});
};
const tagName = XHR.response.keyword;
Receives the data returned as a response and assigns it to the variable tagName when the server-side processing is successful. Use the response property to receive the data.
In this way, let's display them in order below
There are four steps to display the tag.
I'm getting an element with an id name called search-result
The element for displaying the tag is generated using the createElement method. The tag name of the search result is specified for the generated element.
The element prepared in 2 is inserted in the element of 1. It uses the innerHTML property and the appendChild method, respectively.
Iterative processing is performed using forEach
tag.js
XHR.send();
XHR.onload = () => {
const tagName = XHR.response.keyword;
const searchResult = document.getElementById("search-result");
tagName.forEach((tag) => {
//The reason for using forEach is the rails search action
//So, I will put out multiple tags that got caught in the search
//Because in some cases
const childElement = document.createElement("div");
// 2.Creating an element to display the tag
//As the name suggests,Method to create element
childElement.setAttribute("class", "child");
childElement.setAttribute("id", tag.id);
//Class in the created div tag,Give id
//I'm using the tag of a local variable created by forEach here
childElement.innerHTML = tag.tag_name;
// <div>tagname</div>Feeling
//With innerHTML,
//Replace, rewrite, or insert the contents
// 3.The tag of the tag returned from the server side_name
//Image to put in childElement
searchResult.appendChild(childElement);
//html search-As a child element of result
//childElements line up
//I will display it for the first time here
});
};
});
});
};
new.html.erb
<%= form_with model: @tweet, url: tweets_path, class:'form-wrap', local: true do |f| %>
<div class='message-form'>
<div class="message-field">
<%= f.label :message, "Tweet" %>
<%= f.text_area :message, class:"input-message" %>
</div>
<div class="tag-field", id='tag-field'>
<%= f.label :name, "tag" %>
<%= f.text_field :name, class:"input-tag" %>
</div>
<div id="search-result">
</div>
</div>
<div class="submit-post">
<%= f.submit "Send", class: "submit-btn" %>
</div>
<% end %>
so
<div id="search-result">
</div>
,
tag.js
const searchResult = document.getElementById("search-result");
Get it with, perform the above processing, and display the candidates below every time you enter something
Specify the click event for the element displaying the tag. When clicked, enter the tag name in the form and remove the element that displays the tag
tag.js
XHR.send();
XHR.onload = () => {
const tagName = XHR.response.keyword;
const searchResult = document.getElementById("search-result");
tagName.forEach((tag) => {
const childElement = document.createElement("div");
childElement.setAttribute("class", "child");
childElement.setAttribute("id", tag.id);
childElement.innerHTML = tag.name;
searchResult.appendChild(childElement);
const clickElement = document.getElementById(tag.id);
clickElement.addEventListener("click", () => {
document.getElementById("tweets_tag_name").value = clickElement.textContent;
clickElement.remove();
});
});
};
});
});
};
The whole picture looks like this
const clickElement = document.getElementById(tag.id);
//Get the elements of the predictive conversion column that are displayed in order under the tag input form generated earlier
clickElement.addEventListener("click", () => {
//Click the acquired element to fire the event
document.getElementById("tweets_tag_name").value = clickElement.textContent;
// tweets_tag_name is form_The id given to the input form with with
//Get input form
//further.By setting it to value, it was actually entered
//Get value
//clickElement has the name of the tag
// .You can get the name of the tag with textContent
//Now when you click on the tag part, the name of the tag will be
//Enter the form
clickElement.remove();
//Only the clicked tag disappears
However, the same tag will continue to be displayed many times. The reason for this is that every time an incremental search is performed, the latest search results are added while keeping the previous search results. Make sure to delete the previous search results each time an incremental search is performed.
By specifying an empty character string for the innerHTML property of the element in which the search result is inserted, the displayed tag will be deleted.
tag.js
if (location.pathname.match("tweets/new")){
document.addEventListener("DOMContentLoaded", () => {
#abridgement
XHR.send();
XHR.onload = () => {
const tagName = XHR.response.keyword;
const searchResult = document.getElementById("search-result");
searchResult.innerHTML = "";
//In the innerHTML property of the element inserting the search result
//On the other hand, it is displayed by specifying an empty character string.
//Erase the tag
//Of course, when this process is called for the first time, there is nothing, so an empty string is fine.
//When called for the second time, search-result is empty
tagName.forEach((tag) => {
const childElement = document.createElement("div");
childElement.setAttribute("class", "child");
childElement.setAttribute("id", tag.id);
childElement.innerHTML = tag.name;
searchResult.appendChild(childElement);
const clickElement = document.getElementById(tag.id);
clickElement.addEventListener("click", () => {
document.getElementById("tweets_tag_name").value = clickElement.textContent;
clickElement.remove();
});
});
};
});
});
};
Incremental search is supposed to work when something is entered in the form. However, the keyup specified for this event will also fire even with a "key that does not enter characters even if pressed" such as the backspace key.
As a result, since there is no string to use for the search, there is no data in the response, and an error occurs because we are trying to define a non-existent one in tagName. Let's process to display the tag only when there is data in the response.
If you try to define tagName even if there is no data in the response, an error will occur because XHR.response is null. Let's modify so that the tag is displayed only when there is data in the response. Use the if statement to solve the problem as shown below.
tag.js
if (location.pathname.match("tweets/new")){
document.addEventListener("DOMContentLoaded", () => {
#abridgement
XHR.send();
XHR.onload = () => {
const searchResult = document.getElementById("search-result");
searchResult.innerHTML = "";
if (XHR.response) {
//The keyup specified for the event is the backspace key
//Even if you press a key such as, no characters are entered, it will ignite.
//An error occurs if you define a non-existent one in tagName
//Let's process to display the tag only when there is data in the response
const tagName = XHR.response.keyword;
tagName.forEach((tag) => {
const childElement = document.createElement("div");
childElement.setAttribute("class", "child");
childElement.setAttribute("id", tag.id);
childElement.innerHTML = tag.name;
searchResult.appendChild(childElement);
const clickElement = document.getElementById(tag.id);
clickElement.addEventListener("click", () => {
document.getElementById("tweets_tag_name").value = clickElement.textContent;
clickElement.remove();
});
});
};
};
});
});
};
This completes the implementation. Thank you for your hard work.
if (location.pathname.match("drinks/new")){
// location.pathname is
//Get or change the path of the URL of the current page
// .match returns the matching result of the string passed as an argument
//Currently drinks/Event fire when in new
document.addEventListener("DOMContentLoaded",()=>{
//addEventListener executes various event processing
//Methods that can be
//document is the whole html element
// DOMContentLoaded"Is
//Activated when the web page load is completed
//The range of event firing is wide ...?
const inputElement = document.getElementById("tweet_tag_name")
inputElement.addEventListener("keyup",()=>{
//When you fill out the form and the keyboard is released
//Event will be fired in sequence
const keyword = document.getElementById("tweet_tag_name").value;
//Get the value entered in the text box
const XHR = new XMLHttpRequest();
//XHLHttpRequest is an object to enable Ajax on the server
//HTTP requests can be made asynchronously
//Create an instance and assign it to a variable
XHR.open("GET",`search/?keyword=${keyword}`,true);
//open specifies the type of request
//First argument HTTP method specification
//Specifying the second argument path
//Third argument Asynchronous communication ON/OFF
//With a GET request
// ?You can pass parameters with
// ?keyword is a key${keyword}Is the value
//The query parameter is http://sample.jp/?name=Like tanaka
//A URL parameter that spells information after the "?".
//The structure after "?"?<Variable name>=<value>It has become.
// ?Used when you want to search for character strings
//I want to move the search action
//The reason why drinks is omitted
//Based on the directory one level above the specified path
//You can specify a relative path
//For the time being, drinks#Send a request to search
//I want to predictively convert
XHR.responseType = "json";
//The format of the data returned from the controller
//Good compatibility with js and easy to handle as data
//json format is specified
XHR.send();
// tag.I want to send from js to the server side
//Now that we have defined the request
//Let's describe the process to send
XHR.onload = () => {
const searchResult = document.getElementById("search-result");
// 1.The place to display the tag,search-get result
searchResult.innerHTML = "";
//The same tag stays displayed many times
//I want to delete the previous search result
//In the innerHTML property of the element inserting the search result
//On the other hand, it is displayed by specifying an empty character string.
//Erase the tag
//Of course, when this process is called for the first time, there is nothing, so an empty string is fine.
//When called for the second time, search-result is empty
if (XHR.response){
//The keyup specified for the event is the backspace key
//Even if you press a key such as, no characters are entered, it will ignite.
//An error occurs if you define a non-existent one in tagName
//Let's process to display the tag only when there is data in the response
const tagName = XHR.response.keyword;
//When server-side processing is successful
//The data returned as a response
//Receive,Assign to variable
//To receive data
//Use the response property
tagName.forEach((tag) => {
//The reason for using forEach is the rails search action
//So, I will put out multiple tags that got caught in the search
//Because in some cases
const childElement = document.createElement("div");
// 2.Creating an element to display the tag
//As the name suggests,Method to create element
childElement.setAttribute("class", "child");
childElement.setAttribute("id", tag.id);
//Class in the created div tag,Give id
//I'm using the tag of a local variable created by forEach here
childElement.innerHTML = tag.tag_name;
// <div>tagname</div>Feeling
//With innerHTML,
//Replace, rewrite, or insert the contents
// 3.The tag of the tag returned from the server side_name
//Image to put in childElement
searchResult.appendChild(childElement);
//html search-As a child element of result
//childElements line up
//I will display it for the first time here
const clickElement = document.getElementById(tag.id);
//I want the clicked tag name to be entered in the form
//Once you enter,id = tag.html element of id div
//Should be done, so get it
clickElement.addEventListener("click",()=>{
//event fires when the clickElement element is clicked
document.getElementById("tweet_tag_name").value = clickElement.textContent;
// form_Get the element of id made with with
//further.By setting it to value, it was actually entered
//Get value
//clickElement has the name of the tag, so
// .You can get the name of the tag with textContent
//Now when you click on the tag part, the name of the tag will be
//Enter the form
clickElement.remove();
//Only the clicked tag disappears
});
});
};
};
});
});
};
Recommended Posts