フィールドの値を取得してAjaxで指定エレメントへ処理結果を挿入する。

今回はテキストフィールドへ入力されたURLに対して接続確認を行い、結果を表示する処理を作ってみました。ボタンが使いたかったので、link_to_remoteではなくremote_functionを使ってます。

やりたいこと

・テキストフィールドの入力値を取得してアクションへ渡す。
・ボタンを2度押し出来ないようにする。
・loading画像を表示する。
・結果を指定エレメントに表示する。

コード

●controller

def test
  if site_check(params[:url])
    render :text => "<span class='success'>接続に成功しました。</span>"
  else
    render :text => "<span class='failuer'>接続に失敗しました。URLを確認して下さい。</span>"
  end
end

private

def site_check(url, limit = 10)
  return false if limit == 0
  begin
    response = Net::HTTP.get_response(URI.parse(url))
  rescue
    return false
  else
    case response
    when Net::HTTPSuccess
      return true
    when Net::HTTPRedirection
      site_check(response['Location'], limit - 1)
    else
      return false
    end
  end
end

contorllerでは受け取ったurlに対してNet::HTTPで接続確認を行い、結果に応じてrender :text でテキストを直接返してます。なので今回はtestに対応するviewは用意してませんが、viewを使う場合はlayoutを使用しないよう以下のようにcontrollerへ記述しておかないとデザインがおかしな事になります。

 class SitesController < ActionController::Base
   layout "sites", :except => [:test]
 end

Net::HTTPを使った処理は殆どlibrary net/httpのままです。


●view

〜略〜
<%= javascript_include_tag "prototype" %>
<%= stylesheet_link_tag "style" %>
〜略〜

<%= text_field 'site', 'url', :size => 50 %>
<div id='result'></div>
<input type="button" value="TEST" id="testBtn" title="入力したURLの接続テストが行えます"
       onclick="<%= remote_function :update => "result",
                                    :url => { :action => 'test' },
                                    :with => "'url=' + $F('site_url')",
                                    :loading => "$('testBtn').disabled='disabled';
                                                 Element.addClassName('testBtn','disabled');
                                                 Element.update('result','');
                                                 Element.addClassName('result','loading');",
                                    :loaded => "Element.removeClassName('result','loading');
                                                Element.removeClassName('testBtn','disabled');
                                                $('testBtn').disabled=false; "%>">

何は無くともprototype.jsを読み込むのを忘れずに。今回はボタンが使いたかったのでremote_functiononclickイベントに設定していますが、link_to_remoteを使ってもオプションの指定は同じです。各オプションは以下のとおりです。

:update 更新対象のエレメントIDを指定します。今回の場合、空のdiv要素が更新されます。
:url ajaxで接続するアクションを指定します。
:with パラメータとして渡す値を指定します。prototype.js$F()関数を使用してテキストフィールドのvalue値を取得しています。
:loading リクエストの応答待ち中に実行する処理を指定します。まず、ボタンをdisabledにして、IEだと白抜き画像になってしまうので、classを追加してグレーにしています。その後div要素を空にして、classを指定し背景にloding画像を表示しています。div要素を空にしているのは、2回目の実行をすると前回の結果とlodingが画像がダブって表示されてしまう為です。
:loaded 応答完了後の処理を指定します。classを削除してloading画像を非表示にし、ボタンのdisabledを解除しています。これをしないと実行結果が表示された後も、logind画像が回り続け、ボタンも押せない状態のままです。

このremote_funcitonで以下のようなjavascriptが生成されます。parametersにrails2.0から導入されたCSRF対策の為の認証トークンが付加されてます。

new Ajax.Updater(
  'result', 
  '/sites/test', 
  {
    asynchronous:true, 
    evalScripts:true,
    onLoaded:function(request){
      Element.removeClassName('result','loading');
      Element.removeClassName('testBtn','disabled');
      $('testBtn').disabled=false;
    }, 
    onLoading:function(request){
      $('testBtn').disabled='disabled';
      Element.addClassName('testBtn','disabled');
      Element.update('result','');
      Element.addClassName('result','loading');
    },
    parameters:'url=' + $F('site_url') + '&authenticity_token=' + encodeURIComponent('6c83ca10a63e8eed3d1161d41e0e2fc03a76e761')
  }
)


css

.success{
  color: #00aaff;
}

.failuer{
  color: #ff00aa;
}

.disabled{
  background-color: #eee;
  border: 1px solid #ccc;
}

.loading {
  height: 16px;
  background-image: url("../images/loader.gif");
  background-repeat: no-repeat;
}

cssでは特に気をつける事もないですが、loding画像は空のdiv要素の背景に指定しているのでheightを指定しないと画像が表示されません。

そして

ここまで書いてRuby on Rails Documentationをスクロールしていたら、submit_to_remoteの存在に気が付いてしまいました...orz
subumit_to_remoteで書くとこんな感じです。ちょっとシンプル。APIはよく読めって事ですね。

<%= text_field 'site', 'url', :size => 50 %>
<div id='result'></div>
<%= submit_to_remote 'testBtn', 'TEST', :update => "result",
                                        :url => { :action => 'test' },
                                        :with => "'url=' + $F('site_url')",
                                        :loading => "$('testBtn').disabled='disabled';
                                                     Element.addClassName('testBtn','disabled');
                                                     Element.update('result','');
                                                     Element.addClassName('result','loading');",
                                        :loaded => "Element.removeClassName('testBtn','disabled');
                                                    Element.removeClassName('result','loading');
                                                    $('testBtn').disabled=false;",
                                        :html => { :id => 'testBtn', :title => '入力したURLの接続テストが行えます' } %>

まあsubmit_to_remoteにしてもlink_to_limoteにしても、内部的にはremote_functionを使ってるので、remote_functionの使い方を理解しておくのは良い事だという事で。

def link_to_remote(name, options = {}, html_options = nil)
  link_to_function(name, remote_function(options), html_options || options.delete(:html))
end

def submit_to_remote(name, value, options = {})
  options[:with] ||= 'Form.serialize(this.form)'

  options[:html] ||= {}
  options[:html][:type] = 'button'
  options[:html][:onclick] = "#{remote_function(options)}; return false;"
  options[:html][:name] = name
  options[:html][:value] = value

  tag("input", options[:html], false)
end