railsでタグ付け機能を実装して、後半ではJavaScriptで発展的なタグ付けをしましょう
今回は、このようにタグを入力できる機能と、タグを入力するたびに予測変換が下に表示される機能を実装していきたいと思います!
画像で言うと、tagの入力フォームに「酸」と打ったら、下に「酸味」って予測変換的な物が表示されています
ただ、ブラウザが賢いので、ブラウザも予測変換出しちゃってますが、、、笑
下記コマンドを実行
ターミナル
% cd ~/projects
% rails _6.0.0_ new tagtweet -d mysql
% cd tagtweet
データベース作成
データベースを作成する前に、database.ymlに記載されているencodingの設定を変更しましょう。
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
んで、データベース作成
ターミナル
rails db:create
で
Created database 'tagtweet_development'
Created database 'tagtweet_test'
が作成される
データベース設計
tweet と tagは多対多の関係なので、
中間テーブルの
tweet_tag_relationsテーブルを作成するってのがポイント
モデルを作成
ターミナル
% 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
# messegeカラムを追加
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
# nameカラムを追加
t.timestamps
end
end
end
今回は、タグの名前の重複を避けるために「uniqueness: true」という制約を設定します。
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
tweet_tag_relationsテーブルでは、「tweetsテーブル」と「tagsテーブル」の情報を参照するので「foreign_key: true」としています。
ターミナル
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
今回のアプリの仕様
何かつぶやくと,「つぶやき(tweet)」と「タグ(tag)」が同時に保存される仕様を目指します。
このような実装をする時に便利なのがFormオブジェクトというものです。
Formオブジェクト
Formオブジェクトは、1つのフォーム送信で複数のモデルを更新するときに使用するツールです。自分で定義したクラスをモデルのように扱うことができます。
このFormオブジェクトは、「ActiveModel::Model」というモジュールを読み込むことで使うことができます。
ActiveModel::Model
「ActiveModel::Model」とは、Active Recordの場合と同様に「form_for」や「render」などのヘルパーメソッドを使えるようになるツールです。
また、「モデル名の調査」や「バリデーション」の機能も使えるようになります。
Fromオブジェクトを導入
まずはmodelsディレクトリにtweets_tag.rbを作成しましょう
app/models/tweets_tag.rbという配置です。
tweets_tag.rb
class TweetsTag
include ActiveModel::Model
# include ActiveModel::Modelを記述することでFromオブジェクトを作る
attr_accessor :message, :name
# ゲッターとセッターの役割両方できる仮想的な属性を作成
# :nameとかt保存したいカラムを書けば、保存できるって理解でまずはok
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
# saveメソッド内で、格テーブルに値を保存する処理を記述
end
一意性の制約はモデル単位で設ける必要があるため、tagモデルに記述しましょう。
tag.rb
class Tag < ApplicationRecord
has_many :tweet_tag_relations
has_many :tweets, through: :tweet_tag_relations
validates :name, uniqueness: true
end
コントローラーを作成して編集をしましょう
ターミナル
% 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
「Formオブジェクト」に対してnewメソッドを使用しています。
Fromオブジェクトで定義したsaveメソッドを使ってる
ビューの作成
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, "つぶやき" %>
<%= f.text_area :message, class:"input-message" %>
</div>
<div class="tag-field", id='tag-field'>
<%= f.label :name, "タグ" %>
<%= 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は省略!!!
tweets_tag.rbを編集
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
を解説していきます
first_or_initializeメソッドは、whereメソッドと一緒に使います。
whereメソッドは,
モデル.where(条件)のように、引数部分に条件を指定することで、テーブル内の「条件に一致したレコードのインスタンス」を配列の形で取得できます。
引数の条件には、「検索対象となるカラム」を必ず含めて、条件式を記述します。
whereで検索した条件のレコードがあれば、そのレコードのインスタンスを返し、なければ新しくインスタンスを
作るメソッドです
とりあえずこれでタグ付けツイートの実装が完了しました
すでにデータベースへ保存されてるタグをタグ付けしたい場合、入力の途中で入力文字と一致するタグを候補として画面上に表示できる検索機能があれば、より便利なアプリケーションになりそうです
逐次検索機能を実装
逐次検索機能とは、「rails」というタグがすでにデータベースに存在する場合、「r」の文字が入力されると、「r」の文字と一致する「rails」を候補としてリアルタイムで画面上に表示するっていうよくあるやつ
プログラミングで実装するときは** インクリメンタルサーチ**って言われるらしい
それでは実装していきましょう、と言いたいところですが、
application.js
require("@rails/ujs").start()
// require("turbolinks").start() //この行をコメントアウトする
require("@rails/activestorage").start()
require("channels")
上記の行をコメントアウトしないと、jsで設定したイベントが発火しないケースがあるので、コメントアウトしとくのが無難
インクリメンタルサーチ実装の準備
tweets_controller
class TweetsController < ApplicationController
# 省略
def search
return nil if params[:keyword] == ""
tag = Tag.where(['name LIKE ?', "%#{params[:keyword]}%"] )
render json:{ keyword: tag }
end
とサーチアクションを定義
LIKE句は、曖昧な文字列の検索をするときに使用するものでwhereメソッドと一緒に使います
%は空白文字列含む任意の文字列を含む
要するに、params[:keyword]で受け取った値を条件に、nameカラムにその条件が一致するか、tagテーブルで検索した物をtagに代入
それをjson形式で、keywordをキーにして、tagを値にして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
ルーティングをネストする (入れ子にする) ことで、この親子関係をルーティングで表すことができるようになります。
collectionとmember
collectionとmemberは、ルーティングを設定する際に使用できます。
これを使用すると、生成されるルーティングのURLと実行されるコントローラーを任意にカスタムできます。
collectionはルーティングに:idがつかない、memberは:idがつくという違いがあります。
今回の検索機能の場合、詳細ページのような:idを指定して特定のページに行く必要が無いため、collectionを使用してルーティングを設定しましょう
tag.jsを作成しましょう
app/javascript/packsはいかにtag.jsを作成しましょう
application.js
を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("読み込み完了");
});
};
location.pathnameは現在ページのURLを取得、
.matchは引数に渡された文字列のマッチング結果を返す
つまり現在tweets/newにいるときにイベント発火!
documentはhtml要素全体
addEventListenerは様々なイベント処理を実行
DOMContentLoadedはwebページ読み込み完了したときに
つまり、html要素全体が読み込みされたときに、イベントを実行
コンソールに「読み込み完了」と表示されたらok
タグの検索に必要な情報を取得
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;
});
});
};
tweets_tag_nameというidを持ったhtml要素を取得し、InputElementに代入
** ここで注意!!**
form_withによるidの付与
tweets_tag_nameっていったidを持った要素あったっけ??
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, "つぶやき" %>
<%= f.text_area :message, class:"input-message" %>
</div>
<div class="tag-field", id='tag-field'>
<%= f.label :name, "タグ" %>
<%= 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 %>
にも、index.html.erbにもそんなidはありません。。。。。
でもなぜ取得できるか?結論からいうと
form_withが勝手にidを付与してくれるから
詳しくいうと、例えば、
form_with model: @tweetは
tweets_controller
で
def new
@tweet = TweetsTag.new
end
と定義されてあり、
まず、idがtweet_tagになる
そして、
drinks/new.html.erbの
<%= f.label :name, "タグ" %>
<%= f.text_field :name, class:"input-tag" %>
:nameが
tweet_tag にくっ付いて,tweet_tag_name
ってidが生成されます!!
「どこの誰がいったことか信じられねーよ!!」って意見ももっともなので
実際に検証ツールで form_withによってidが生成されてるかどうか調べます
つぶやきをツイートするmessageの場所には
tweets_tag_messagesというidが生成されて、それが、
<%= f.text_area :message, class:"input-message" %>
に付与されます。
tag付けをする場所は
tweets_tag_nameというidが生成されて、それが
<%= f.text_field :name, class:"input-tag" %>
に付与されます。
form_withによってidが付与される!!!
ってことを頭に入れておいてください
これで、入力フォームが取得できました
変数keywordの中身を確認
app/javascript/packs/tag.js
if (location.pathname.match("tweets/new")){
document.addEventListener("DOMContentLoaded", () => {
const inputElement = document.getElementById("tweets_tag_name");
// form_withで生成されたidをもとに入力フォームそのものを取得
inputElement.addEventListener("keyup", () => {
// 入力フォームからキーボードのキーが離されたときにイベント発火
const keyword = document.getElementById("tweets_tag_name").value;
// .valueとすることで、入力フォームに入力された値を取り出すことができる
// 実際に入力された値を取得して、keywordに入力
console.log(keyword);
});
});
};
ここまできたら、フォームに何か入力してみましょう。
入力した文字がコンソールに出力できていればokです。
XMLHttpRequestオブジェクトを生成
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();は
XMLHttpRequestオブジェクトを用いてインスタンスを生成し、変数XHRに代入しましょう
非同期通信に必要なXMLHttpRequestオブジェクトを生成しましょう。
XMLHttpRequestオブジェクトを用いることで、任意のHTTPリクエストを送信できます。
openメソッドを用いてリクエストを定義
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);
openメソッドの第一引数にHTTPメソッド、第二引数にURL、第三引数には非同期通信であることを示すためにtrueを指定しましょう。
なぜこういうURLの指定になるかと言うと,
このURLはqueryパラメーターといって,http://sample.jp/?name=tanakaのように、
「?」以降に情報を綴るURLパラメーターです。
「?」以降の構造は、**?<変数名>=<値>**となっています。
今回は:idとかでtweetsを識別する必要がないので、queryパラメーターを指定する
drinks#searchを動かしたいのに、searchがなぜURLで省略されてるのか
指定したパスの一個上のディレクトリを基準に,相対的にパスを指定できるから
例えば、今回指定したパスはsearch/keyword=hogehoge
で、一個上のディレクトリはtweetsなので、
一個上のディレクトリを勝手に補完してくれるらしい。。。。
これで、Drinks#searchを動かせる
と、思ったが、
XHR.responseType = "json";
を書いて、コントローラーから返却されるデータの形式にjson形式を指定しましょう
そして最後!
XHR.send();
を書いて、リクエストを送信しましょう.
タグの入力フォームに何かしら入力されるたびに、railsのsearch アクションが動くといった形になってます!
サーバーサイドからのレスポンスを受け取りましょう
サーバーサイドの処理が成功したときにレスポンスとして返ってくるデータを受け取りましょう。データの受け取りには、responseプロパティを使用します。
tag.js
if (location.pathname.match("tweets/new")){
document.addEventListener("DOMContentLoaded", () => {
# 省略
XHR.send();
XHR.onload = () => {
const tagName = XHR.response.keyword;
};
});
});
};
const tagName = XHR.response.keyword;
は、サーバーサイドの処理が成功したときに、レスポンスとして返ってくるデータを受け取って変数tagNameに代入してます
データの受け取りにはresponseプロパティを使用します。
#### タグを表示させる処理を記述しましょう
このように、下に順に表示させていきましょう
タグを表示させる手順は以下の4つです。
1. タグを表示させる場所を取得する
search-resultと言うid名がついた要素を取得しています
- タグ名を格納させる場所を取得する。
createElementメソッドを用いてタグを表示させるための要素を生成しています。
生成した要素に検索結果のタグ名を指定しています。
- 2の要素にタグを挿入する
2で用意した要素を1の要素に挿入しています。
それぞれinnerHTMLプロパティとappendChildメソッドを用いています。
- 2と3の処理を検索結果があるだけ繰り返す
forEachを使って、繰り返し処理を行っています
tag.js
XHR.send();
XHR.onload = () => {
const tagName = XHR.response.keyword;
const searchResult = document.getElementById("search-result");
tagName.forEach((tag) => {
// forEachを使う理由は、railsのsearchアクション
// で、検索に引っかかったタグを、複数出していく
// 場合もあるので
const childElement = document.createElement("div");
// 2.タグを表示させるための要素を生成してる
// 名前の通り,要素を作るメソッド
childElement.setAttribute("class", "child");
childElement.setAttribute("id", tag.id);
// 作ったdivタグにclass,idを付与する
// forEachで作られたローカル変数のtagをここで使ってる
childElement.innerHTML = tag.tag_name;
// <div>tagname</div> って感じ
// innerHTML を使用すると、
// 中身を入れ替えたり、書き換えたり、入れたりする
// 3.サーバーサイドから返ってきたtagのtag_name
// をchildElementの中に入れてくイメージ
searchResult.appendChild(childElement);
// htmlのsearch-resultの子要素に
// childElementが並んでく
// ここで初めて表示していく
});
};
});
});
};
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, "つぶやき" %>
<%= f.text_area :message, class:"input-message" %>
</div>
<div class="tag-field", id='tag-field'>
<%= f.label :name, "タグ" %>
<%= 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 %>
で
<div id="search-result">
</div>
を、
tag.js
const searchResult = document.getElementById("search-result");
で取得して、上記のような処理をおこなって、何か入力するたび候補を下に表示します
クリックしたタグ名がフォームに入力されるようにしましょう
タグを表示している要素にクリックイベントを指定します。
クリックされたら、フォームにタグ名を入力して、タグを表示してう要素を削除するようにしましょう
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();
});
});
};
});
});
};
全体像こんな感じ
const clickElement = document.getElementById(tag.id);
// さっき生成したタグ入力フォームの下に順に表示されていく、予測変換の欄の要素を取得
clickElement.addEventListener("click", () => {
// 取得した要素をクリックすると、イベント発火
document.getElementById("tweets_tag_name").value = clickElement.textContent;
// tweets_tag_nameはform_withで入力フォームに付与されるid
// 入力フォームを取得
// さらに.valueとすることで、実際に入力された
// 値を取得
// clickElementはタグの名前があるので
// .textContentでタグの名前を取得できる
// これでタグの部分をクリックしたら、タグの名前が
// フォームに入ってく
clickElement.remove();
// クリックしたタグのみ消える
しかし、このままだと同じタグが何度も表示されたままになってしまいます。
この原因は、インクリメンタルサーチが行われるたびに、前回の検索結果を残したまま最新の検索結果を追加してしまうからです。
インクリメンタルサーチが行われるたびに、直前の検索結果を消すようにしましょう。
直前の結果検索を消すようにしましょう
検索結果を挿入している要素のinnerHTMLプロパティに対して、空の文字列を指定することで、表示されているタグを消します。
tag.js
if (location.pathname.match("tweets/new")){
document.addEventListener("DOMContentLoaded", () => {
# 省略
XHR.send();
XHR.onload = () => {
const tagName = XHR.response.keyword;
const searchResult = document.getElementById("search-result");
searchResult.innerHTML = "";
// 検索結果を挿入してる要素のinnerHTMLプロパティに
// 対して、空の文字列を指定することで、表示されてる
// タグを消します
// 最初にこの処理が呼び出される時は当然何もないので空文字でいいし
// 2回目に呼び出された時は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();
});
});
};
});
});
};
フォームに何も入力しなかった場合のエラーを解消する
本来、インクリメンタルサーチはフォームに何か入力された場合に動作する想定です。しかし、今回イベントに指定したkeyupは、バックスペースキーなどの「押しても文字入力されないキー」でも発火してしまいます。
その結果、検索に使用する文字列がないため、レスポンスにデータが存在せず、存在しないものをtagNameに定義しようとしているのでエラーが発生してしまいます。
レスポンスにデータが存在する場合のみ、タグを表示させる処理が行われるようにしましょう。
レスポンスにデータが存在しない場合にもtagNameを定義しようとすると、XHR.responseがnullなのでエラーが発生してしまいます。レスポンスにデータが存在する場合のみ、タグを表示させる処理が行われるように修正しましょう。以下のようにif文を用いて解消します。
tag.js
if (location.pathname.match("tweets/new")){
document.addEventListener("DOMContentLoaded", () => {
# 省略
XHR.send();
XHR.onload = () => {
const searchResult = document.getElementById("search-result");
searchResult.innerHTML = "";
if (XHR.response) {
// イベントに指定したkeyupは、バックスペースキー
// などの押しても文字入力されないキーでも発火してしまう
// 存在しないものをtagNameに定義するとエラーが起こる
// レスポンスにデータがある場合のみタグを表示させる処理を行おう
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();
});
});
};
};
});
});
};
これで実装完了です。お疲れ様でした。
tag.jsのコードのまとめ
if (location.pathname.match("drinks/new")){
// location.pathnameは
// 現在ページのURLのパスを取得、変更
// .matchは引数に渡された文字列のマッチング結果を返す
// 現在drinks/new にいる時にイベント発火
document.addEventListener("DOMContentLoaded",()=>{
// addEventListenerは様々なイベント処理を実行
// することができるメソッド
// documentはhtml要素全体
// DOMContentLoaded"は
// Webページ読み込みが完了した時に発動
// イベント発火する範囲広くね、、、?
const inputElement = document.getElementById("tweet_tag_name")
inputElement.addEventListener("keyup",()=>{
// フォームに入力して、キーボードが離されたタイミング
// で順次イベント発火していく
const keyword = document.getElementById("tweet_tag_name").value;
// テキストボックスの入力した値を取得
const XHR = new XMLHttpRequest();
// XHLHttpRequest とはAjaxを可能にするためのオブジェクトでサーバーに
// HTTPリクエストを非同期で行うことができます
// インスタンスを生成して、変数に代入する
XHR.open("GET",`search/?keyword=${keyword}`,true);
// openはリクエストの種類を指定する
// 第一引数 HTTPメソッドの指定
// 第二引数 パスの指定
// 第三引数 非同期通信のON/OFF
// GETリクエストで、
// ?でパラメーターを渡せる
// ?keywordはキーで、${keyword}が値
// queryパラメーターとは、http://sample.jp/?name=tanakaのように、
// 「?」以降に情報を綴るURLパラメーターです。
// 「?」以降の構造は、?<変数名>=<値>となっています。
// ?文字列とかの検索をかけたい時に使う
// サーチアクションを動かしたい
// drinksが省略されてる理由は
// 指定したパスの一個上のディレクトリを基準に
// 相対的にパスを指定できる
// とりあえず、drinks#searchにリクエストを送って
// 予測変換したい
XHR.responseType = "json";
// コントローラーから返却されるデータの形式には
// jsと相性がよく、データとして取り扱いやすい
// json形式を指定してる
XHR.send();
// tag.jsからサーバーサイドに送信したい
// リクエストを定義できたので、
// 送信する処理を記述しましょう
XHR.onload = () => {
const searchResult = document.getElementById("search-result");
// 1.タグを表示させる場所である,search-resultを取得
searchResult.innerHTML = "";
// 同じタグが何度も表示されたままになってしまう
// 直前の検索結果を消したい
// 検索結果を挿入してる要素のinnerHTMLプロパティに
// 対して、空の文字列を指定することで、表示されてる
// タグを消します
// 最初にこの処理が呼び出される時は当然何もないので空文字でいいし
// 2回目に呼び出された時はsearch-resultが空になる
if (XHR.response){
// イベントに指定したkeyupは、バックスペースキー
// などの押しても文字入力されないキーでも発火してしまう
// 存在しないものをtagNameに定義するとエラーが起こる
// レスポンスにデータがある場合のみタグを表示させる処理を行おう
const tagName = XHR.response.keyword;
// サーバーサイドの処理が成功した時に
// レスポンスとして返って来るデータを
// 受け取って,変数に代入
// データの受け取りには
// responseプロパティを使用する
tagName.forEach((tag) => {
// forEachを使う理由は、railsのsearchアクション
// で、検索に引っかかったタグを、複数出していく
// 場合もあるので
const childElement = document.createElement("div");
// 2.タグを表示させるための要素を生成してる
// 名前の通り,要素を作るメソッド
childElement.setAttribute("class", "child");
childElement.setAttribute("id", tag.id);
// 作ったdivタグにclass,idを付与する
// forEachで作られたローカル変数のtagをここで使ってる
childElement.innerHTML = tag.tag_name;
// <div>tagname</div> って感じ
// innerHTML を使用すると、
// 中身を入れ替えたり、書き換えたり、入れたりする
// 3.サーバーサイドから返ってきたtagのtag_name
// をchildElementの中に入れてくイメージ
searchResult.appendChild(childElement);
// htmlのsearch-resultの子要素に
// childElementが並んでく
// ここで初めて表示していく
const clickElement = document.getElementById(tag.id);
// クリックしたタグ名がフォームに入力されるようにしたい
// 入力していったら,id = tag.idのdivのhtml要素
// ができているはずなので、それを取得
clickElement.addEventListener("click",()=>{
// clickElement要素をクリックした時にイベント発火
document.getElementById("tweet_tag_name").value = clickElement.textContent;
// form_withで作られたidの要素を取得
// さらに.valueとすることで、実際に入力された
// 値を取得
// clickElementはタグの名前があるので、
// .textContentでタグの名前を取得できる
// これでタグの部分をクリックしたら、タグの名前が
// フォームに入ってく
clickElement.remove();
// クリックしたタグのみ消える
});
});
};
};
});
});
};