Help us understand the problem. What is going on with this article?

Rails, jQuery-ui, Autocompleteで語句候補ドロップダウン

More than 3 years have passed since last update.

Screenshot 2015-07-29 09.18.57.png

やりたいこと

  • 入力フォームに対して、ユーザーが1文字入力するごとに語句候補のドロップダウンリストを表示したい。
  • リストのアイテムをクリックしたら、それが値として入力される。

環境

  • Ruby 2.2.1
  • Rails 4.2.3
  • jquery-rails
  • jquery-ui-rails
  • bootstrap

手順

View

例えば、こんなテンプレートがあるとする。

/app/views/movings/new.html.haml
= form_for(@moving) do |f|

-#...

.form-group
  = f.label :state_from, "* State", class: "h4"
  = f.text_field :state_from, class: "state form-control"

-#...

語句候補のデータを準備する。

いつも同じデータで良いのであれば、予め配列として準備する。
最終的に配列になっていれば、データ準備の方法はどうでもOK。

/app/helpers/movings_helper.rb
module MovingsHelper
  def us_states
    ["Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado",
      "Connecticut", "Delaware", "District of Columbia", "Florida", "Georgia",
      "Hawaii", "Idaho", "Illinois", "Indiana", "Iowa", "Kansas", "Kentucky",
      "Louisiana", "Maine", "Maryland", "Massachusetts", "Michigan", "Minnesota",
      "Mississippi", "Missouri", "Montana", "Nebraska", "Nevada", "New Hampshire",
      "New Jersey", "New Mexico", "New York", "North Carolina", "North Dakota",
      "Ohio", "Oklahoma", "Oregon", "Pennsylvania", "Rhode Island", "South Carolina",
      "South Dakota", "Tennessee", "Texas", "Utah", "Vermont", "Virginia",
      "Washington", "West Virginia", "Wisconsin", "Wyoming"]
  end
end

jQuery

以下のjQueryのコード3行を加えるだけでAutocompleteの実装は完了。source属性に先ほど準備した配列を渡す。

どこか別の場所に保管されているデータを参照しても良いし、

jQuery ->                              # DOMが読み込まれたのを確認
  $('#moving_state_from').autocomplete # 対象となるinputタグのID
    source: #{us_states}               # 語句候補の配列(Rubyコードから渡す場合)

データが少ないのであれば、直接データ(配列)を埋め込んでもOK。

jQuery ->                                     # DOMが読み込まれたのを確認
  $('#moving_state_from').autocomplete        # 対象となるinputタグのID
    source: ["breakfast", "lunch", "dinner"]  # 語句候補の配列(直接記述場合)

他の選択肢で選択された内容に対応するデータに差し替えたい場合。

# 選択された国名に応じて県名データを差し替える。
jQuery ->
  $from = $('#moving_country_from')
  $from.change ->
    switch $from.children("option").filter(":selected").text()
      when "United States" then params = { source: #{us_states} }
      when "Japan"         then params = { source: #{jp_prefectures} }
      else                      params = { source: [] }
    $('#moving_state_from').autocomplete(params)

コントローラで生成したデータを利用する場合。

/app/models/moving.rb
class Moving < ActiveRecord::Base
  #...

  # データをまとめて準備するメソッド
  def autocomplete_suggestions
    {
      items:      Hash[Ingredient.pluck(:name, :volume)],
      rooms:      Room.select(:name).pluck(:name),
      categories: self.moving_items.select(:category).distinct.pluck(:category)
    }
  end
  #...
end
/app/controllers/moving_items_controller.rb
  #...
  def new
    @moving_item = MovingItem.new
    set_autocomplete_suggestions_and_render(:new)
  end

  def create
    @moving_item = MovingItem.new(moving_item_params.merge(moving_id: current_moving))
    if @moving_item.save
      flash[:success] = "Created #{@moving_item.name}"
      redirect_to @moving
    else
      set_autocomplete_suggestions_and_render(:new)
    end
  end
  #...
  private
    #...
    def set_autocomplete_suggestions_and_render(template)
      @suggestions = @moving.autocomplete_suggestions
      render template
    end
  #...
end
new.html.haml
-# ID付きの空タグにデータをdata属性に書き込む
= content_tag :div, "", id: "suggestions", data: @suggestions

= render 'add_form'
movings.coffee
jQuery ->

  setVolume = (volume)->
    $("#moving_item_volume").val(volume)

  setSlider = (volume)->
    $("#volume_slider").val(volume)

  # Slider

  document.getElementById('volume_slider').addEventListener 'change', ->
    setVolume(document.getElementById('volume_slider').value)

  # AutoComplete

  $('#moving_item_name').autocomplete
    # IDでタグを探し、dataメソッドでデータを取り出す。
    source: Object.keys( $('#suggestions').data('items') )
    select: (e, ui) =>
      itemVolume = $('#suggestions').data('items')[ui.item.value]
      setVolume(itemVolume)
      setSlider(itemVolume)

  $('#moving_item_room').autocomplete
    # IDでタグを探し、dataメソッドでデータを取り出す。
    source: $('#suggestions').data('rooms')

  $('#moving_item_category').autocomplete
    # IDでタグを探し、dataメソッドでデータを取り出す。
    source: $('#suggestions').data('categories')

ドロップダウンメニューのスタイリング

自分の好みでスタイリングする。

autocomplete.scss
ul.ui-autocomplete {
  position: absolute;
  list-style: none;
  margin: 0;
  padding: 0;
  border: solid 1px #999;
  cursor: default;
  li {
    background-color: #FFF;
    border-top: solid 1px #DDD;
    margin: 0;
    padding: 2px 15px;
    a {
      color: #000;
      display: block;
      padding: 3px;
    }
    a.ui-state-hover, a.ui-state-active {
      background-color: #FFFCB2;
    }
  }
}

Autocomplete実装後のHTML

Screenshot 2015-07-29 09.41.37.png

テスト

Gemfile
group :test do
  gem 'rspec-rails'
  gem 'capybara'
  gem "poltergeist"
end
/spec/rails_helper.rb
# ...
require 'capybara/poltergeist'
Capybara.javascript_driver = :poltergeist

options = { js_errors: false }
Capybara.register_driver :poltergeist do |app|
  Capybara::Poltergeist::Driver.new(app, options)
end
# ...
/spec/features/autocomplete_spec.rb
require 'rails_helper'

feature "Autocomplete interface", type: :feature, js: true, driver: :poltergeist do

  let(:user) { create(:user) }

  before do
    log_in_as user
    visit root_path
  end

  describe "movings/new" do

    before do
      first(:link, "New moving").click
      select "United States", from: "moving_country_to"
    end

    it "has US states suggestion data" do
      expect(page).to have_css("div#moving_suggestions")
      expect(page.find('div#moving_suggestions')["data-us-states"]).to have_content("Alabama")
    end

    it "shows autocomplete with a correct suggestion" do
      fill_autocomplete("moving_state_to", with: "New")
      expect(page).to have_content("New Mexico")
      expect(page).to have_content("New York")
      expect(page).not_to have_content("Alabama")
    end
  end
end

def fill_autocomplete(field, options = {})
  fill_in field, with: options[:with]

  page.execute_script %Q{ $('##{field}').trigger('focus') }
  page.execute_script %Q{ $('##{field}').trigger('keydown') }

  selector = %Q{ ul.ui-autocomplete li.ui-menu-item a:contains("#{options[:select]}") }

  expect(page).to have_selector('ul.ui-autocomplete')
  page.execute_script %Q{ $('#{selector}').trigger('mouseenter').click() }
end

参考資料

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした