prototype.js で簡単な Ajax のサンプル

適当な感想を言うだけなのもなんなので例を書きます。これを作る。higeponにあやかりたいあやかりたい。
いまさら人に聞けないAjaxと簡単なサンプル - higepon blog


・画面上テキストラベルに見える部分をマウスクリックすると編集可能になる
・文字列を入力し、Enterキーを押すと、テキストがバックグラウンドで更新され、ラベル表示に戻る。
・画面はリロードされない。

完成品はこれ。
http://childtv.org/prototype/sample.html
使っているのは Prototype 1.3.1 です。
http://prototype.conio.net/dist/prototype-1.3.1.js

sample.html

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "">http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja">
<head>
<meta http-equiv="Content-type" content="text/html; charset=UTF-8" />
<meta http-equiv="Content-Script-Type" content="text/javascript" />
<script type="text/javascript" src="prototype.js"></script>
<script type="text/javascript" src="EditableTextBox.js"></script>
<script type="text/javascript">
Event.observe(window, 'load', function() {
    var label = new EditableTextBox($('label'));
    label.onSubmit = function() {
        new Ajax.Request('/hatenak/keyword', {
            method:     'get',
            parameters: 'word=' + encodeURIComponent(label.getLabel()),
            onSuccess:  function(transport) {
                alert(transport.responseText);
            }
        });
    };
}, false);
</script>
<title>prototype.js で 簡単な Ajax のサンプル</title>
</head>
<body>
    <div id="label">デフォルトの値</div>
</body>
</html>

EditableTextBox.js

var EditableTextBox = Class.create();
EditableTextBox.prototype = {
    // 初期化
    // @param label テキストまたはブロックエレメント
    initialize: function(label) {
        // テキストラベルのエレメント
        this.text = document.createElement('span');
        Event.observe(this.text, 'click', this.edit.bindAsEventListener(this), false);

        // 編集してる時のエレメント
        this.form = document.createElement('form');
        this.input = document.createElement('input');
        this.input.type = 'text';
        this.form.appendChild(this.input);
        Event.observe(this.form, 'submit', this.submit.bindAsEventListener(this), false);

        this.setLabel*1;
        if(this.onSubmit) this.onSubmit.call(this, event);
        Event.stop(event);
    }
};

大まかな解説。EditableTextBox.js で編集可能なテキストラベルのコンポーネントを定義しています。編集を確定すると onSubmit が呼ばれるので、そこで行う Ajax な処理を sample.html に書いてる。まあこのくらいなら higepon がやってるみたいにベタっと書いたほうが早いんだけど、使い方を解説するにはこっちのがいいかなあということで。
ではまず短い sample.html から見ていきましょう。head 内で prototype.js と EditableTextBox.js を読んでる。

<script type="text/javascript" src="prototype.js"></script>
<script type="text/javascript" src="EditableTextBox.js"></script>

本題は次の script タグの中身。HTML の中身が読み終わらないうちに実行されると困るので、prototype.js が提供する Event.observe を使ってロード時に初期化が実行されるようにしています。Event.observe の使い方は addEventListener と同じです。

Event.observe(element, type, observer, useCapture)

初期化は、まず EditableTextBox を作ってからそれの onSubmit をセットする。でコンストラクタの引数がなにやら見慣れぬ風だと思うのですが

var label = new EditableTextBox($('label'));

これは関数名が '$' である関数で、文字列が渡されると getElementById でエレメントを、エレメントが渡されるとエレメントそのままを返します。これを使うと id 名と要素をシームレスに扱えるわけです。ここでは body 内にある div#label をコンストラクタに渡しています。そしてみんな大好き Ajax が onSubmit の中ででてくるのですが、これはごく簡単。

new Ajax.Request('/hatenak/keyword', {
    method:     'get',
    parameters: 'word=' + encodeURIComponent(label.getLabel()),
    onSuccess:  function(transport) {
        alert(transport.responseText);
    }
})

URL と method やハンドラを Ajax.Request に与えるだけ。'get' を 'post' にすればそのまま POST もできます。ハンドラは readyState に合わせて onUninitialized, onLoading, onLoaded, onInteractive, onComplete と、完了した時に on + (ステータスコード) か onSuccess, onFailure が呼ばれる。ちゃんと通信できた時だけを扱うならこれみたいに onSuccess を書いて置けばいい。引数の transport は XMLHttpRequest オブジェクトです。これで sample.html は終わり。
EditableTextBox が書いてある EditableTextBox.js の説明に移ります。このコンポーネント

  • 単なるラベル
  • 編集

の2つの状態を持っていて、それぞれに対応するエレメント text と form をとっかえひっかえ表示します。ではさっそくコードを見てみましょう。

var EditableTextBox = Class.create();

これは前回も書いたように

function EditableTextBox() {
    this.initialize(arguments);
}

を表しています。initialize の中では各エレメントを作成します。ラベルのエレメントを作る様子を見てみましょう。

// テキストラベルのエレメント
this.text = document.createElement('span');
Event.observe(this.text, 'click', this.edit.bindAsEventListener(this), false);

Event.observe はさっき見たとおりで、ここではラベルがクリックされた時に編集モードに移るよう指示しています。問題は

this.edit.bindAsEventListener(this)

ってのは何かということです。this.edit はこれの下で定義している、ラベルと編集のエレメントを入れ替えるメソッド。

edit: function(event) {
    this.element.replaceChild(this.form, this.text);
    Field.activate(this.input);  // フォーカス・選択
},

JavaScript ではリスナーとして単なる関数を渡すので、OO な感じで書いている場合その関数がどのオブジェクトに対して呼び出されるかが問題になります。例えば bindうんちゃらと書かずに

Event.observe(this.text, 'click', this.edit, false);

にすると、イベントが起きたとき関数 this.edit は span エレメントである this.text に対して呼び出されて、そうすると関数内の this が想定外なのでエラーになります。ちゃんと現在の EditableTextBox オブジェクトに対して関数が呼び出されるようにするには prototype.js に書いてあるこれを使うといい。

Function.prototype.bind = function(object) {
  var __method = this;
  return function() {
    __method.apply(object, arguments);
  }
}

Function を拡張して、与えられたオブジェクトに対して自身が呼び出される関数を作る bind メソッドを作っています。

var func = this.edit.bind(this);
func();

こうすると func() は this.edit() と同じになる。これで this がどっかいっちゃう問題は解決。だけどイベントのオブザーバーとして関数を渡す場合にはもう一歩便利なものがあるのです。

Function.prototype.bindAsEventListener = function(object) {
  var __method = this;
  return function(event) {
    __method.call(object, event || window.event);
  }
}

W3C のイベントモデルではオブザーバーに引数として渡されたイベントオブジェクトから色々情報を得たりするんだけど、IE では引数の代わりにグローバル変数 window.event を使う。そこの差を埋めてくれるわけです。これを使ったのがさっきのやつ。

Event.observe(this.text, 'click', this.edit.bindAsEventListener(this), false);

もう全部わかったね。よかったね。残りは困る部分はないと思います。$F が Form.Element.getValue の略記でこれは form の要素の値を得る関数だとか、Event.stop が preventDefault と stopPropagation だとかは prototype.js を検索してください。おわり。

*1:typeof label == 'string') ? label : label.innerHTML); // EditableTextBox 全体のエレメント if(typeof label == 'string') { this.element = document.createElement('div'); } else { this.element = label; this.element.innerHTML = ''; } this.element.appendChild(this.text); }, // 書いてある文字のアクセサ getLabel: function() { return $F(this.input); // return this.input.value と同じ }, setLabel: function(label) { this.text.innerHTML = label; this.input.value = label; }, // 編集開始 edit: function(event) { this.element.replaceChild(this.form, this.text); Field.activate(this.input); // フォーカス・選択 }, // 編集終わり // もしあれば this.onSubmit を呼ぶ submit: function(event) { this.element.replaceChild(this.text, this.form); this.setLabel($F(this.input