TapestryのAjaxサポート

Tapestryには、洗練されたJavaScriptとAjaxへの対応があります。 これは PrototypeScriptaculous をベースにしています。 これらのライブラリはTapestry内に組み込まれているので、別途ダウンロードしてくる必要はありません。

Tapestryの目標は、多くの基本的で便利なコンポーネントをアプリケーションで利用できるようにすることと、 他のJavaScriptウィジェットをTapestryコンポーネントとして簡単にカプセル化できるようにすることです。

Ajaxへの対応は新しいコンポーネントを追加するという形で行われますが、 時にはコンポーネントMixinとして行われることもあります。

Prototypeに対する変更点

Tapestryは現在、Prototype 1.6.0.3 を使用しています。

Scriptaculousに対する変更点

普通のScriptaculousには固有のスクリプトロード機能があります。 Scriptaculousのメインライブラリである scriptaculous.js をロードすると、ライブラリ内の他のライブラリも すべて ロードされます。 通常は "load" クエリパラメータを使ってこれを微調整することができます。

これはTapestryにはうまく適用できません; この後述べるように、 TapestryはページにロードするJavaScriptライブラリを個々のコンポーネントが制御できるようになっています。 さらに、必要なスクリプトの正確な組み合わせはページをレンダリングする経路によって決まります。 そしてそれは、あるコンポーネントがレンダリングされる必要があるかどうかによります。

Scriptaculousのメインライブラリ scriptaculous.js の自動ロード機能は働かないように修正してあります。 Tapestryは prototype.js, scriptaculous.js, effects.js およびTapestry用のライブラリである tapestry.js に対するリンクを自動生成します。その他のライブラリも必要に応じて追加することができます。

TapestryにおけるJavaScriptの扱い方の基本

Tapestryにおける一般的な戦略は、どんなJavaScriptであっても静的なJavaScriptライブラリとして .js ファイルにパッケージし、クライアントからダウンロードできるようにすべきということです。

ページ固有のJavaScriptは、オブジェクトを初期化しJavaScriptライブラリを参照するだけの最小限の記述にとどめるべきです。

この大部分は RenderSupport オブジェクトを通して行います。

RenderSupport にはコンポーネントが使用できるいくつかのメソッドがあります。 (訳注:原文の "event" がどこに係るのかわからず後半が訳せなかった→ RenderSupport include a number of methods that will be used by components, or event by services that are called from components.)

addScriptLink()

void addScriptLink(Asset... scriptAssets);

このメソッドはJavaScriptライブラリのファイルへのリンクを追加します。 コンポーネントにひとつまたは複数のスクリプトをインジェクトし、そのAssetをこのメソッドに渡すことができます。 Tapestryは <link> 要素をドキュメントの 先頭 (<head>要素の内側)に追加します。

同じAssetが複数回追加されても、リンクが重複して作成されることは ありません。2回目以降の追加は単に無視されます。 これにより、各コンポーネントは他のコンポーネントとコンフリクトすることを心配せずに自身が必要とするAssetを追加することができます。

Prototype、Scriptaculousのメインライブラリとeffectsライブラリ、標準Tapestryライブラリ(これは主にフォームの入力値検証のサポートを含んでいます) は自動的に追加されるということを忘れないでください。

Scriptaculousの他のライブラリが必要な場合は次のようにします:

  @Inject @Path("${tapestry.scriptaculous}/dragdrop.js")
  private Asset dragDropLibrary;

  @Environmental
  private RenderSupport renderSupport;

  void setupRender()
  {
    renderSupport.addScriptLink(dragDropLibrary);
  }

Assetが挿入される際に、${tapestry.scriptaculous}シンボルによりScriptaculousライブラリの配置場所が参照されます。

RenderSupportはEnvironmentalサービスによってアクセスすることができます。

setupRender() メソッド(このメソッド名はレンダーフェーズの名前です)は、 必要なライブラリをRenderSupportサービスに知らせるのに適切な場所です。

addScript()

void addScript(String format, Object... arguments);

このメソッドは小さな初期化用JavaScriptをページに追加します。 初期化 とは、それがページの下部に挿入され、クライアント上でドキュメントのロードが完了したときにだけ(すなわち、window.onload イベントハンドラから) 実行されるということを意味します。

このメソッドを使うとき、引数の書式文字列には標準的な書式指示子('%s'など)を含めることができます。 これは String.format() をわざわざ呼び出す手間を省きます。 そして、フォーマット済みのJavaScriptはscriptブロック内に追加されます。

RenderSupportのインジェクト

RenderSupportは 環境 オブジェクトなので、通常は Environmental アノテーションを使ってインジェクトします。

  @Environmental
  private RenderSupport renderSupport;

Environmentalアノテーションはコンポーネント内でのみ機能します。しかし、RenderSupportをサービスが必要とすることもあります。 幸い、RenderSupportのプロキシが用意されているので次のようにすることもできます:

  @Inject
  private RenderSupport renderSupport;

... または、サービスの実装クラスのコンストラクタで次のようにします:

  public MyServiceImpl(RenderSupport support)
  {
    . . .
  }

RenderSupportが(他の環境オブジェクトと同様に)レンダリング中でのみ使用でき アクションリクエスト中においては使用できないことを強調するために、コンポーネント内ではEnvironmentalアノテーションを用いるべきです。

IncludeJavaScriptLibraryアノテーション

IncludeJavaScriptLibrary アノテーションでひとつまたは複数のJavaScriptライブラリを簡単に挿入することができます。

先ほどの例は次のように書き換えることができます。

@IncludeJavaScriptLibrary("${tapestry.scriptaculous}/dragdrop.js")
public class MyComponent
{
 . . .
}

これにより、AssetをインジェクトしRenderSupportを呼び出す手間を省くことができます。 しかし、初期化用JavaScriptを加えるためにRenderSupportのインジェクトは必要となるでしょう。

AjaxコンポーネントとMixin

Autocomplete Mixin

Autocomplete Mixinは、テキストフィールドに入力中のフレーズを補完するためにサーバへ問い合わせることを可能にするものです。 ドロップダウンリストとしてクライアント側にダウンロードするには大きすぎる選択肢から、例えば数千にも上る数の中から、 値をひとつ選ばなければならないフィールドがある場合にこれを使うことができます。

Autocomplete は既存のテキストフィールドに追加することができます:

  <t:textfield t:id="accountName" t:mixins="autocomplete" size="100"/>

Autocomplete Mixin はいくつかの設定値があります。コンポーネントリファレンスを見てください。

ユーザーがフィールド内に文字を入力すると、クライアント側のJavaScriptがサーバにリクエストを送信し補間候補を取得します。

この補間候補を提供するためのイベントハンドラを書かなくてはなりません。イベント名は "providecompletions" です。 ユーザーが入力中の値がコンテキスト値として渡され、戻り値がユーザに提示する選択肢になります。

例:

  List<String> onProvideCompletionsFromAccountName(String partial)
  {
    List<Account> matches = accountDAO.findByPartialAccountName(partial);

    List<String> result = new ArrayList<String>():

    for (Account a : matches)
    {
      result.add(a.getName());
    }

    return result;
  }

ここでは findByPartialAccountName() が返す値はソート済みであると仮定します。 そうでない場合はこの値をソートする必要があるでしょう。 Autocomplete Mixin 自身がソートを行うことは ありません

配列、リストだけでなく、単一のオブジェクトを返すこともできます。文字列以外のオブジェクトを返すことも可能です ... その場合は toString() を用いてクライアント側の文字列に変換されます。

Tapestryのデフォルトスタイルシートには、選択肢用フローティングポップアップの外観を制御するためのエントリがあります。

全体の外観を変更するには DIV.t-autocomplete-menu UL を上書きします。 ポップアップリストの要素は DIV.t-autocomplete-menu LI、 カーソル下の(あるいは矢印キーで選択中の)要素は DIV.t-autocomplete-menu LI.selected です。

Zone

Zoneの初期のサポートが利用できるようになりました。Zoneとは、クライアント側でページの一部だけを更新するためのTapestryにおける手法です。 Zoneコンポーネントは "t-zone" CSSクラスを持った <div> 要素としてレンダリングされます。 また、その <div> 要素の更新を制御するための Tapestry.Zone オブジェクトを結びつけるJavaScriptをページ内に追加します。

ZoneはActionLinkコンポーネントによって更新することができます。ActionLinkにはzoneパラメータがあり、そこにZoneの <div> 要素のidを指定します。 そのActionLinkのリンクをクリックすると、サーバ側のイベントハンドラメソッドが通常通り実行されます ... ただし、イベントハンドラメソッドの戻り値は ページ断片のレスポンス をクライアントに送信するために使用されます。 そして、そのレスポンスの内容でZoneの <div> 要素が更新されます。

Zoneのdivと更新用div

多くの場合、Zoneは動的なコンテンツの外観を提供するための "ラッパー" または "コンテナ" で、 境界を作成するためのちょっとしたラッピングマークアップです。 その場合、Zoneの <div> は更新用 <div> を含むことができます。

更新用 <div> は "t-zone-update" CSSクラスを持った <div> として、Zoneの <div> の 内部 に配置します。

更新の際はZoneの <div> 全体が更新されるのではなく更新用 <div> の内容が更新されます。

表示と更新のJavaScript関数はZoneの <div> に対して作用します。

イベントハンドラの戻り値の型

普通のリクエストでは、イベントハンドラメソッドの戻り値はページを決めるためのもので、 それによりクライアントに リダイレクト が送信され(新たなリクエストとして)新たに 完全な ページがレンダリングされます。

Zoneの更新では、戻り値は ページ断片のレスポンス をレンダリングするのに使われ、それは 同じリクエスト 内で行われます。

この戻り値はインジェクトされたコンポーネントかblockである必要があります。 それがレンダリングされ、そのマークアップがクライアント側でZoneの <div> を更新するのに使われます。

イベントハンドラは Link を返すこともできます。 その場合、クライアントはそのリンクが示す先へリダイレクトします。

また、ページ名(文字列としての)やページクラス、ページのインスタンスを返すと、その戻り値が示すページへリダイレクトします。

JavaScriptが使用できないユーザーへの適切な対応

JavaScriptを使用できないユーザーがZoneを更新するように構成されているActionLinkをクリックするかもしれません。

その場合でもリクエストはやはりサーバ側に送信されます。しかし、普通の リクエストとして処理されます。

JavaScriptを使用できないユーザーにも適切に対応するためには、 それを検出し普通のレスポンス、つまりページオブジェクトやページ名はたはページクラスを返す必要があります。

これを行うには、Request オブジェクトをインジェクトし、 isXHR() メソッドを使います。このメソッドは、Ajaxリクエストのときはtrueを返し普通のリクエストのときはfalseを返します。

Zone用のJavaScript関数

Zoneの初期状態は表示にも非表示にもできます。Zoneが更新されるとき、現在の状態が非表示の場合には表示されます。 これはクライアント側の Tapestry.ElementEffect オブジェクトにある関数によって行われます。 デフォルトでは、Zoneの表示には show() 関数が用いられます。 Tapestry.ElementEffect にある関数の 名前 をZoneのshowパラメータに指定することで変更できます。

Zoneがすでに表示されている場合は、更新されたことを強調するために別の関数が用いられます。 これはZoneのupdateパラメータで指定します。デフォルトは highlight() 関数で、 黄色のフェードによってZoneの内容が更新されたことを強調します。

Zoneの制限

他の多くの場合と異なり、あなたがZoneコンポーネントのidを適切にそして一意に付け、ActionLinkコンポーネントでそのidを参照するようにすることにTapestryは依存します。 そして、Tapestryは(インデックス番号を付け加えることで)あなたが指定したidを 一意なものにする ので、 Zoneコンポーネントをループ内などで使用する場合には新たな問題が生じます。

表示と更新の関数名は小文字に変換されます; そのため、Tapestry.ElementEffect の全ての関数名は小文字にすべきです。 クライアント側のJavaScriptは(既存のオブジェクトに関数を追加できたりと)動的なので、Tapestryは関数名の検証をおこないません。 ... しかし、関数名が不適切な場合にはデフォルトの show() と highlight() が用いられます。

今のところ、ページ断片のレンダリングにはEnvironmentalサービスを利用できません。 これはすぐに変更する予定です。

もうすぐ実装される予定のもの

  • Zoneを用いてFormを拡張する
  • Tapestry.ElementEffect の関数の追加とドキュメント
  • 実際に動くサンプル ...

独自のAjaxコンポーネントの作成

Autocomplete Mixinのコードを調べることはとても役に立ちます; リンクを作成するために ComponentResources オブジェクトをどのように使えばよいかわかるでしょう。

鍵となるのは、コンポーネントのイベントハンドラメソッドをTapestryがどのように実行しているかということです。

Ajaxリクエストでは、イベントハンドラメソッドからの戻り値は通常のアクションリクエストとは異なる方法で処理されます。 通常のリクエストでは戻り値はリダイレクト先のページの名前かページのクラスかページのインスタンスです。

Ajaxリクエストではリダイレクトは送信されません: 同じリクエスト内でレスポンスがレンダリングされ、ただちに返送されます。

イベントハンドラメソッドからの戻り値は、以下の値をとることができます:

  • レスポンスとしてレンダリングするBlockまたはConponent。レンダリングされたマークアップは "content" をキーとするJSONハッシュの値となります。 Zoneコンポーネントを更新するときの戻り値がこれです。
  • JSONObject または JSONArray。その内容がレスポンスとして返されます。
  • StreamResponse。その内容がレスポンスとして返されます。

標準Tapestryライブラリ

ファイル tapestry.js が標準Tapestryライブラリで、Tapestryにおけるクライアント側の処理サポートします。 これは、PrototypeとScriptaculousのEffectsライブラリに依存しています。 あなたのコード内で他のJavaScriptやJavaScriptライブラリを使用すると、tapestry.js とその依存ファイルは自動的にページに追加されます。

Tapestryの名前空間

TapestryはTapestryの名前空間内に多数のオブジェクトやクラスを定義しています。

また、FormクラスとForm要素にいくつかのメソッドを追加しています。これらの大部分は入力検証と要素の表示/非表示に関するものです。

Tapestryオブジェクト

標準Tapestryライブラリは $T() という関数を追加します。この関数はPrototypeの $() と似ていますが、 DOMオブジェクトを返す代わりにそのDOMオブジェクトに関連付けられた(空のJavaScriptオブジェクトで初期化された)ハッシュを返します。 このハッシュが Tapestryオブジェクト です。

この関数には、オブジェクトのid(文字列として)かオブジェクトの参照を渡すことができます。Tapestryオブジェクトは、最初の呼び出し時に生成されます。 メモ: DOMオブジェクト内に _tapestry という名前のプロパティを見つけることができるでしょう(これはデバッグ時に役に立ちます)。

TapestryがDOMオブジェクトに情報を追加する場合、それはTapestryオブジェクトに対して行われます。 これにより名前の衝突を避け、またTapestryが追加したプロパティを一カ所にグループ化することでデバッグが容易になります。

例えば、ある要素に必要な値を次のように保持し:

  $T(myid).fadeDuration = .5;

この値を他の場所で使用します:

  new Effect.Fade($(myId), { duration: $T(myid).fadeDuration });