4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

railsとjsを用いてタグ付け機能を実装してみる

Last updated at Posted at 2020-12-18

railsでタグ付け機能を実装して、後半ではJavaScriptで発展的なタグ付けをしましょう

スクリーンショット 2020-12-18 14.36.29.png

今回は、このようにタグを入力できる機能と、タグを入力するたびに予測変換が下に表示される機能を実装していきたいと思います!

画像で言うと、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が生成されてるかどうか調べます

スクリーンショット 2020-12-13 6.43.28.png

つぶやきをツイートする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プロパティを使用します。

#### タグを表示させる処理を記述しましょう

スクリーンショット 2020-12-18 14.36.29.png

このように、下に順に表示させていきましょう

タグを表示させる手順は以下の4つです。

1. タグを表示させる場所を取得する

search-resultと言うid名がついた要素を取得しています

  1. タグ名を格納させる場所を取得する。

createElementメソッドを用いてタグを表示させるための要素を生成しています。
生成した要素に検索結果のタグ名を指定しています。

  1. 2の要素にタグを挿入する

2で用意した要素を1の要素に挿入しています。
それぞれinnerHTMLプロパティとappendChildメソッドを用いています。

  1. 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();
            // クリックしたタグのみ消える
          });
          });
        };
      };
    });
  });
};
4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?