Rails4/jQuery/JSONで郵便番号から住所を自動入力する具体的実装内容


jQueryとかの非同期通信の技術tipsは断片的にはよく見ますが、
Ruby on Railsを使って一連の実装が説明されているものがあまりない・・・。
ということで、郵便番号から住所の自動入力を実装した内容をメモしておきます。


#やりたいこと

- ボタンをクリックするとWEBページを遷移することなく郵便番号をGETにてサーバへ送信。
- マスターデータの郵便番号が重複していたら各郵便番号の"共通している部分まで"を取得。
- 郵便番号が無かった場合「該当の郵便番号は登録されていない」旨を表示。
- サーバからJSONで住所データを受け取って住所記述用のテキストフォームに埋め込む。

#今回の開発環境

```ruby:Gemfile
ruby '2.0.0'
gem 'rails', '4.0.5'
gem 'jquery-rails', '3.0.4'
gem 'coffee-rails', '4.0.1'
gem 'bootstrap-sass','3.2.0.2'    # 任意（bootstrap3用）
gem 'sass-rails', '4.0.2'         # 任意（bootstrap3用）
gem 'haml-rails', '0.5.3'         # 任意（HAML用）
```

※動作確認等は主にchromeで行っています。

***

#実装内容

Model, Controller, View（Ajax）についてそれぞれ実装方法を見ていきます。
※マスターデータは以前「[Rails4の郵便番号/住所変換のマスターデータ作成手順](http://qiita.com/inodev/items/8a9230a3f245967f6e22)」で作成した物を使用します。

##1. Modelの実装





Controllerでdbから得た情報を操作する（欲しい情報を加工する）処理を書くことは、可読性を下げてあまり良くありません。
そこで、まずmodelに欲しい情報を返却させるメソッドを記述します。
マスターデータの郵便番号は重複している可能性があるので、
今回は郵便番号が複数あった場合に共通した情報（重複している文字列）のみを返すメソッドとして実装しておきます。

例えば郵便番号「9870385」に該当するレコードは以下の３つです。

- 4（宮城県）, 登米市, 豊里町下九
- 4（宮城県）, 登米市, 豊里町内田
- 4（宮城県）, 登米市, 豊里町内畑

このとき、該当郵便番号では『4（宮城県）,登米市,豊里町』を返却するデータとします。

###処理内容

```ruby:app/models/area.rb
class Area < ActiveRecord::Base
  belongs_to :prefectural

  # 郵便番号に応じてJSONで渡すHashデータを取得するメソッド
  def self.search_area(search_code)
    areas = self.where("postal_code = ?", search_code)
    if areas.empty?                     # 該当なし
      resp_hash = nil
    else                                # 該当あり
      area_info = areas.first
      areas.drop(1).each do |next_area| # 該当が複数あれば不要な情報を削る
        area_info.prefectural_id = 0 if area_info.prefectural_id != next_area.prefectural_id
        area_info.city   = self.samestr(area_info.city, next_area.city)
        area_info.street = self.samestr(area_info.street, next_area.street)
      end
      ### 返却データの整形 ###
      resp_hash = area_info.attributes  # 編集出来るようにHashとして取得
      resp_hash.delete("id")            # 返却不要な要素の削除
      resp_hash.delete("postal_code")
    end
    return resp_hash
  end

  private
    # ２つの文字列の始めから同じ部分を取得するメソッド
    def self.samestr(str1, str2)
      return "" if str1.nil? || str2.nil?                # レコードの空要素を読んだ時のnil避け
      samechars = ""
      str1, str2 = str2, str1 if str1.length > str2.length # 最大でも小さい方の最後までしか比較しない
      0.upto(str1.length-1) do |index|
        str1[index] == str2[index] ? samechars += str1[index] : break
      end
      return samechars
    end

end
```

Areaモデルのクラスメソッドとして"search_area"を定義します。
郵便番号を渡すとフォームに書き込む（JSONとして渡す）住所データを返すよう実装します。
RailsではControllerの`render`メソッドで、オブジェクトをJSONで渡すように指定すればそのまま渡してくれるのですが、
渡したくない不要な要素を削除したかったので、一旦dbから引っ張ってきたAreaのオブジェクトをHashオブジェクトに変換して整形しています。



##2. Controllerの実装


### ルーティング設定

Controllerで郵便番号をGETにて受け取れるようにする為に、まずはルーティング設定を行っておきます。

```ruby:config/routes
  get "members/get_area"
```

### 実際のレスポンス処理

```ruby:app/controllers/members_controller.rb
class MembersController < ApplicationController
  ### 中略 ###

  def get_area
    records = Area.search_area(params[:search_code])
    render json: records
  end
end
```

先ほど用意したArea#search_areaに、ルーティングで許可したプロパティを渡します。
Hashで渡したいデータが返ってくるので、そのままrenderメソッドにJSONとして指定してやれば良いだけです。

###動作確認

指定したルーティングに引数を指定してアクセスすると、渡されるJSONデータを見ることが出来ます。
![スクリーンショット 2014-09-21 23.54.39.png](https://qiita-image-store.s3.amazonaws.com/0/52535/f1067fa7-aa23-4e12-42c5-d673d87751e9.png "スクリーンショット 2014-09-21 23.54.39.png")

##3. View及びAjaxの実装

ここでは処理を俯瞰しやすいよう一部の関連のある処理のみ記述します。

###作成フォームのイメージ

![viewイメージ.jpg](https://qiita-image-store.s3.amazonaws.com/0/52535/43c644e1-95d7-0c4a-1d9a-53aaf48b462e.jpeg "viewイメージ.jpg")

### Viewのフォーム作成
viewファイル（HAML）を下記のように書いてフォーム部分を作成します。
localeのymlにて該当modelのカラム名を日本語記述している為、form_for内のf.labelでは自動で表示されています。
また、県名はコントローラで`@prefectural = Prefectural.all`によって全県リストを取得してきています。
※Viewの使用例はdeviseによるユーザー情報編集用のページの為URLがやや特殊になっています。

```html:app/views/members/registrations/edit.html.haml
        = form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f|

          <!---- 中略 ---->

          .form-group
            .row
              .col-sm-4
                = f.label :address_postal_code
              .col-sm-8
                <!---- 結果MSG書き込み用のdiv ---->
                #postalcode-result
            .row
              .col-sm-4
                <!---- 郵便番号入力フォーム ---->
                = f.text_field :address_postal_code, maxlength: 7, class: "form-control address"
              .col-sm-8
                <!---- 郵便番号送信＆JSON受信ボタン ---->
                .btn.btn-default#getarea-button <i class="fa fa-map-marker"></i> 郵便番号から住所を入力
          .form-group
            = f.label :address_prefectural, class: "control-label"
            %br/
            <!---- 県名のセレクトによる選択 ---->
            = f.select :address_prefectural, @prefectural.collect{|t|[t.name,t.id]}, {prompt: "選択して下さい"}, class: "form-control address"
          .form-group
            = f.label :address_city
            %br/
            <!---- 市区町村の入力フォーム ---->
            = f.text_field :address_city, class: "form-control address"
          .form-group
            = f.label :address_street
            %br/
            <!---- 通りの入力フォーム ---->
            = f.text_field :address_street, class: "form-control address"
          .form-group
            = f.label :address_building
            %br/
            <!---- 建物の入力フォーム ---->
            = f.text_field :address_building, class: "form-control address"

```


###CoffeeScriptの実装

次の処理をjQueryなどで作ってみます。

- 郵便番号入力フォームにて数値のみ入力させる規制処理
- 郵便番号をサーバに投げて、返ってきた住所データを書き込むAjax処理
- エラーや結果を書き込むDHTML処理


```coffeescript:app/asset/javascript/members.js.coffee
$(document).on 'ready page:load', -> # turbolinks対策
  ### 郵便番号入力フォーム：数値のみの入力 ###
  $('#member_address_postal_code').keydown ->
    presskey = String.fromCharCode(event.keyCode);
    event.returnValue = /[0-9\b\t\n]/.test(presskey) # 入力受付キーを正規表現で指定

  ### 住所取得ボタン：Ajaxで住所取得 ###
  $('#getarea-button').click ->
    postalcode = $('#member_address_postal_code').val()
    if postalcode.length != 7
      $("#postalcode-result").css("color","#ff0000").html("郵便番号は７桁です。")
      return false
    $.ajax
      async:    true                             # true:非同期通信
      url:      "/members/get_area/"
      type:     "GET"
      data:     {search_code: postalcode}
      dataType: "json"
      context:  this
      error: (jqXHR, textStatus, errorThrown) -> # 通信/サーバエラーなど
        $("#postalcode-result").css("color","#ff0000").html(errorThrown)
      success:  (data, textStatus, jqXHR) ->
        if data?                                 # 対象あり
          $("#member_address_prefectural").val(data.prefectural_id) # 県名index更新
          $("#member_address_city").val(data.city)                  # 市区町村名更新
          $("#member_address_street").val(data.street)              # 通り名更新
          $("#postalcode-result").css("color","#00dd00").html("入力が完了しました。")
        else                                     # 対象なし
          $("#postalcode-result").css("color","#ff0000").html("未登録の郵便番号です。")

  ### MSGのリセット ###
  $('.address').change -> $("#postalcode-result").html("")
```

CoffeeScriptでAjaxとして直接Actionを動作させています。
応答のJSONデータで、そのまま各IDを便りにフォームを更新しています。
CSS等に分割すればCoffeeScriptはもう少しだけ奇麗に書けそうな気はします。。。



##動作確認
####未入力or桁不足のとき/郵便番号がないとき
![ng.jpg](https://qiita-image-store.s3.amazonaws.com/0/52535/51600e9d-bf8e-2226-a4ef-41b9b0895d08.jpeg "ng.jpg")


####入力された郵便番号があるとき
![ok.jpg](https://qiita-image-store.s3.amazonaws.com/0/52535/86caf718-2f10-a5ad-acbc-e5d5f8b97939.jpeg "ok.jpg")

※下記のように一部同じ郵便番号で違う県の場合全て空になります。そこは妥協しています。
- 4980000 愛知県 弥富市 
- 4980000 三重県 桑名郡木曽岬町 



特にjQueryは勉強し始めたばかりなので、もっと良い書き方などあれば教えて頂ければ幸いです！