Ext JS 5 の ウィジェットを理解する
こんにちは、ゼノフィnakamuraです。
Ext JS 5 は、新しい “widgetcolumn” を使うことでグリッドセル内のコンポーネントへの対応を始めました。それと同時に、Ext JS 5 ではウィジェット (Widget)というライトウェイトなコンポーネントを新しく導入しました。Ext JS 5 には幾つかのウィジェットが含まれており、この記事は自作のものを作るのがいかに簡単かを解説します。
キーコンセプトをわかりやすくするために、簡単な「レイティング (ratings)」ウィジェットを作ります。以下のようなものです。

始めましょう
Ext.Component から派生する普通のコンポーネントとは違い、ウィジェットは新しいベースクラス Ext.Widget から派生します。Ext.Widget から派生したクラスは、ほぼ完全にコンフィグシステムで定義されます (詳しくは後ほど) が、 Ext.Widget は DOM エレメントがどう生成され、DOM イベントがメソッドにどうバインドされるかを定義します。
レンダリング
ウィジェットを使う上で最初に検討することは、DOM ツリーを定義することです。これは、クラス宣言内で “element” プロパティを指定します。
Ext.define('Ext.ux.rating.Picker', { extend: 'Ext.Widget', //... element: { cls: 'ux-rating-picker', reference: 'element', children: [{ reference: 'innerEl', cls: 'ux-rating-picker-inner', listeners: { click: 'onClick', mousemove: 'onMouseMove', mouseenter: 'onMouseEnter', mouseleave: 'onMouseLeave' }, children: [{ reference: 'valueEl', cls: 'ux-rating-picker-value' },{ reference: 'trackerEl', cls: 'ux-rating-picker-tracker' }] }] }, //... }); |
“element” オブジェクトは基本的には DOM エレメントを作成するための Ext.dom.Helper 指定です。追加された主要なものは “reference” と “listeners” プロパティです。 ViewController をご存じなら、これらの名前は聞き覚えのあるものかもしれません。ウィジェットでも同じような機能を果たします。Ext.Widget では、 “reference” プロパティを持ったすべてのエレメントはウィジェットインスタンスにキャッシュされます。その場合、プロパティの値が名前に使われます (例: “element”、“innerEl”など) 。
イベント
上記の “element” 記述子では、“innerEl” オブジェクト上で “listeners” オブジェクトが定義されていることがわかります。これらのリスナーは指定された部分から作られたエレメントにアタッチします。そのメソッドはウィジェットクラス上で名前により見つけることができます。例えば、次の様になります。
Ext.define('Ext.ux.rating.Picker', { extend: 'Ext.Widget', //... onClick: function (event) { var value = this.valueFromEvent(event); this.setValue(value); }, onMouseEnter: function () { this.element.addCls(this.overCls); }, onMouseLeave: function () { this.element.removeCls(this.overCls); }, onMouseMove: function (event) { var value = this.valueFromEvent(event); this.setTrackingValue(value); }, |
これは一般的なコンポーネントクラスを書く場合に似ていますが、初期化とクリーンアップコードがないことには驚くかもしれません。Ext.Widget コンストラクターはエレメントを作り、それらの参照をトラッキングし、リスナーも設定してくれます。これらの処理に関しては (対応する destroy イメソッドでも) Ext.Widget は追加のライフサイクルや関連するオーバーヘッドはありません。
その代わり、派生クラスでは、コンフィグシステムを使用して、 “config” プロパティを提供することでどういった動作をするかを定義します。まだコンフィグシステムについてあまり知らない人のために、ここで少し触れておこうと思います。
コンフィグシステム 101
Ext JS の核にある概念の1つとして、“config” プロパティというコンセプトがあります。Ext JS が始まった時から存在していますが、Ext JS 5 (Sencha Touch 2.x でも) からこれらのプロパティのしくみが明確になりました。公式の “config” は以下のように宣言します。
Ext.define('Ext.ux.rating.Picker', { //... config: { family: 'monospace' } //... }); |
上記の宣言は次の手書きのコードと基本的に同等のものです。
Ext.define('Ext.ux.rating.Picker', { //... getFamily: function () { return this._family; }, setFamily: function (newValue) { var oldValue = this._family; if (this.applyTitle) { newValue = this.applyFamily(newValue, oldValue); // #1 if (newValue === undefined) { return this; } } if (newValue !== oldValue) { this._family = newValue; if (this.updateFamily) { this.updateFamily(newValue, oldValue); // #2 } } return this; }, //... }); |
これらの自動化の主な利点は次の通りです。
- 明確さ – コードが少ない分クラスが読みやすくなっています。
- 一貫性 – すべての config は同じ動作をします。
- 柔軟性 – 正しく設定すれば、config プロパティはどんな時でも変更をすることができます (過去の Ext JS では作成時にのみ変更ができるという制限がありました) 。
任意ではありますが、開発者が “family” などのプロパティに提供できる2つの重要なメソッドがあります。applyFamily と updateFamily (上記の #1と #2) です。オーバーライドする際には、get や set ではなく、これらのメソッドが大体オーバーライドされます。
アプライヤーapply メソッドを使うことで開発者は受け取った値を、実際に保存する値に変換することができます。多くの apply メソッドでは、受け取ったコンフィグオブジェクトによってクラスのインスタンスを作成するということをしたり、単に内部表現の標準化を一箇所にして、プロパティが使われるすべての場所でチェックしなくてもいいようにしたりします。
アップデーターconfig プロパティが値を変えた場合、update メソッドがコールされます。古い値を新しい値に変更するのは update メソッドの責任です。
initConfig – すべてを1つに最後に、クラスを config システムに参加させるには、どこかの時点で initConfig メソッドをコールする必要があります。Ext.Widget では、コンストラクター内で行われています。initConfig メソッドは config オブジェクトを受け入れ、各プロパティ、そしてクラス上で宣言されたものを実行するために適切な set、apply、update メソッドをコールします。
このメソッドは config プロパティ間での順序の問題を解決してくれる “just in time” セットアップメカニズムも提供してくれます。例えば、ある config プロパティの update メソッドが他の config の値を必要とした場合、もう1つの config の get メソッドをコールするだけです。その裏では、initConfig がリクエストされたプロパティに、正しいset/apply/update シーケンスが呼び出されているかを、結果を返す前に確認しています。
cachedConfig で最適化
ウィジェットにとって、多くの config は DOM を同じように操作します。与えられたウィジェットのインスタンスが、デフォルトの config の値をオーバーライドすることはないはずなので、そういったプロセスの結果がキャッシュ化されることが理想的です。これらの config のためには、クラスを少し変更することができます。
Ext.define('Ext.panel.Panel', { //... cachedConfig: { family: 'monospace' } //... }); |
多くの場合、これらの config は普通の config と同じです。しかし、キャッシュ化された config の場合、クラスの最初のインスタンスが作成された時に config システムは普段より少し多めに処理を行います。
最初のインスタンス
最初のインスタンスの config オブジェクトを実行する前に、config システムはその最初のインスタンスをクラスのデフォルト値を使って初期化します。このプロセスは様々な apply や update メソッドを呼び出し、その結果 “element” 指定によって作成された DOM エレメントをアップデートします。
“family” config には次のアップデーターがあるとします。
updateFamily: function (family) { this.element.setStyle('fontFamily', "'" + family + "'"); }, |
全てのアップデーターはウィジェットの DOM のデフォルト状態に寄与します。config がデフォルト値にセットされてると、afterCachedConfig メソッドがコールされます。最初のインスタンスの場合だけ、このメソッド内で Ext.Widget は生成された DOM ツリーを掘り下げてクローンします (cloneNode(true) DOM API を使います) 。
2つめのインスタンス (そしてそれ以降)
同じウィジェットクラスにて 2つめのインスタンスを作る時、Ext.Widget は DOM ツリーのキャッシュされたクローンを、掘り下げてクローンすることで新しいウィジェットの DOM ツリーを作成します。これにより、エレメント指定とデフォルト値のためにアップデーターを再実行する手間を省きます。もし config アップデーターが正しく書かれていれば、このプロセスは非常にわかりやすいものになります。
もちろん Ext.Widget は DOM ツリーを複製してからも仕事があります。例えばエレメントの参照を取り出したり、リスナーを結びつけたり、インスタンスに渡されたデフォルト値でない config を設定したりなどです。しかし、これらのコストはクラスへの全ての config ではなく、インスタンスに与えられた config 値の数に直接関係しています。
再利用、リサイクル
単一のウィジェットが作られ、初期化されるところを見たので、ここからはウィジェットを widgetcolumn で使用する上での重要なコンセプトについてふれていきたいと思います。
作成するインスタンスの数を制限するのは常に大事なことですから、バッファード レンダリングが鍵になります。このアプローチを使用することで、グリッドはレコードよりも少ないウィジェットインスタンスをレンダリングし、行がスクロール領域の「後方」で削除された場合はリサイクルし、新しい行が「前方」でレンダリングされます。
これらの移行が行われる時、widgetcolumn は DOM内のウィジェットを新しい行に移動し、対応するレコードの dataIndex から指定されたフィールドを読み込み、defaultBindProperty をセットするためにウィジェット上の setConfig をコールします。これは apply と update メソッドを実行することになるので、正しくコーディングされていれば、ウィジェットは新しいフィールドの値を表すものとして再設定されます。
今回のウィジェットの場合、編集可能な値を表しているため、updateValue メソッド内を見て ウィジェットがグリッドセル内で使われているかを確認しなければなりません。
column = me.getWidgetColumn && me.getWidgetColumn(); record = column && me.getWidgetRecord && me.getWidgetRecord(); if (record && column.dataIndex) { record.set(column.dataIndex, value); } |
getWidgetColumn と getWidgetRecord メソッドは widgetcolumn によってウィジェット上に配置されます。それはグリッド内のコンテキストを知るためです。
結論
多くの話がグリッドに関連したものでしたが、ウィジェットは一般的なコンポーネントが使われる場所ならどこでも使うことができます。これは Ext JS 5 から導入されたレイティング ウィジェットやスパークライン ウィジェットでも同じです。
これは例に挙げたアプリのメインパネルのクリーンショットです。”items” 配列から4つのインスタンスが表示されています。

もし上記のことに聞き覚えがあるものがあれば、それは Sencha Touch の形式をご存じだからかもしれません。Ext JS 5 で拡張されたものも多少はありますが、Ext.Widget は根本的には Sencha Touch にあった Ext.AbstractComponent の最新版です。
では、どんな場合にコンポーネントの代わりにウィジェットを作るのでしょうか?ウィジェットを書くのは、コンポーネントを書くことよりも簡単なものです。特にレイアウト要件が CSS だけで処理できる場合は簡単です。また、将来的にウィジェットは、Sencha モバイルとデスクトップフレームワークを統合していく中で、クロスオーバー機能を持ちます。