Ext JS 5 の宣言型リスナー
こんにちは、ゼノフィnakamuraです。
過去に掲載された「 Using ViewControllers in Ext JS 5 (日本語訳: Ext JS 5 の ViewController を使う) 」という記事では、Ext JS 5 で大幅に改良された「宣言型イベントリスナー」という機能について軽く触れました。この記事では宣言型リスナーをどの様に使えばアプリケーションのビューをより簡潔にすることができ、カスタム コンポーネント内でのボイラープレート コードを減らすことができるかをより詳しく解説していきます。
なお、この記事はExt JS 5.0.1 以降を使用していることを前提に書かれています。
宣言型リスナーとは何か?
「宣言型リスナー」というのは、クラスボディーやインスタンス化するときのコンフィグオブジェクトで、 listeners コンフィグを使って宣言されたリスナーの事を指します。 リスナーをこういった形式で宣言する機能は Ext JS 5 から新たに導入されたわけではありません。Ext JS 4 でも、クラスにはリスナーを宣言できましたが、ハンドラー関数やスコープが定義されているだけでした。 例えば。
Ext.define('MyApp.view.User', { extend: 'Ext.panel.Panel', listeners: { // 関数はインラインまたは事前に定義します collapse: function() { // パネルが畳まれたときの反応 } }, // このメソッドは collapse ハンドラーとしては定義できません onCollapse: function() { } }); |
要求するハンドラー関数は、通常はクラスの定義時点では利用できないなため、宣言型リスナーは Ext JS 4 では限定的な用途しかありませんでした。開発者は on をオーバーライドし、on メソッドを使用することでリスナーを追加していました。
Ext.define('MyApp.view.User', { extend: 'Ext.panel.Panel', initComponent: function() { this.callParent(); this.on({ collapse: this.onCollapse, scope: this }); }, onCollapse: function() { console.log(this); // Panel のインスタンス } }); |
スコープの解決
我々は Ext JS 5 では listeners コンフィグを改良し、 イベント ハンドラーをメソッド名に対応する文字列として指定できるようにしました。フレームワークは、実行時にこれらのメソッドの名前を具体的な関数への参照に解決します。我々はこのプロセスをリスナースコープ解決 (listner scope resolution) と呼びます。
Ext JS 4 では、明示的に “scope” が与えられなければ、文字列ハンドラーを解決することができませんでした。Ext JS 5 では、明示的なスコープが宣言されていない文字列リスナーでも、デフォルトでスコープを解決できる、特別なルールを新たに追加しました。
スコープ解決には二つの結果が存在し得ます。コンポーネントもしくは ViewController です。チェックはコンポーネントを探す作業から始まります。コンポーネントもしくは ViewController がスコープとなりますが、チェックの結果スコープではなかった場合、フレームワークはコンポーネントの階層を、適切なコンポーネントもしくは ViewController が見つかるまでのぼって行きます。
スコープがコンポーネントになる場合
フレームワークがスコープを解決するためにとる最初の手段は、 defaultListenerScope コンフィグが true にセットされたコンポーネントを探すことです。クラスで宣言されているリスナーの場合、コンポーネント自身でサーチが始まります。
Ext.define('MyApp.view.user.User', { extend: 'Ext.panel.Panel', xtype: 'user', defaultListenerScope: true, listeners: { save: 'onUserSave' }, onUserSave: function() { console.log('user saved'); } }); |
このリスナーは User ビューのクラスボディで宣言されています。つまり、フレームワークは 階層をさかのぼる前に User ビュー自身の defaultListenerScope をチェックします。この場合、User ビューの defaultListenerScope コンフィグが true にセットされているため、このリスナーのスコープは User ビューであると解決します。
インスタンス化の際のコンフィグで宣言されたリスナーの場合、コンポーネント自身のチェックは飛ばされ、フレームワークは階層をのぼって親コンテナからサーチを始めます。次の例をご覧ください。
Ext.define('MyApp.view.main.Main', { extend: 'Ext.container.Container', defaultListenerScope: true, items: [{ xtype: 'user', listeners: { remove: 'onUserRemove' } }], onUserRemove: function() { console.log('user removed'); } }); |
このリスナーは User ビューの 「インスタンス コンフィグ」に宣言されています。その結果、フレームワークは User ビューを飛ばし( defaultListenerScope 構成が trueにセットされていたとしても)、Main ビューにさかのぼる事で解決します。
スコープが ViewControllers になる場合
Ext JS 5 では、新しいタイプのコントローラー、Ext.app.ViewController が登場しました。ViewController についての詳細は、「 Using ViewControllers in Ext JS 5 (日本語訳: Ext JS 5 の ViewController を使う) 」で紹介したので、ここでは ViewController に関連するイベントリスナーにだけ焦点をあてたいと思います。
たくさんの View を管理できる Ext.app.Controller と違い、それぞれの ViewController インスタンスは単一の View インスタンスとバインドされています。この View と ViewController 間の 一対一の関係が ViewController を、View や View のアイテムに宣言された listeneres のデフォルトのスコープとしての役割を果たします。
defaultListenerScope と同じルールが ViewController にも適応されます。クラスレベルのリスナーは、階層をさかのぼる前に、コンポーネント自体の ViewController を探します。
Ext.define('MyApp.view.user.User', { extend: 'Ext.panel.Panel', controller: 'user', xtype: 'user', listeners: { save: 'onUserSave' } }); Ext.define('MyApp.view.user.UserController', { extend: 'Ext.app.ViewController', alias: 'controller.user', onUserSave: function() { console.log('user saved'); } }); |
上記のリスナーは User ビューのクラスボディーに宣言されています。User ビューが独自のコントローラーを持っているため、フレームワークはスコープが UserController であると解決します。もし User ビューが独自のコントローラーを持っていなければ、スコープは階層をさかのぼって解決します。
一方では、インスタンスレベルの listeners はコンポーネントを飛ばして、階層の上の ViewController で解決します(親コンテナから始めます)。例えば。
Ext.define('MyApp.view.main.Main', { extend: 'Ext.container.Container', controller: 'main', items: [{ xtype: 'user', listeners: { remove: 'onUserRemove' } }] }); Ext.define('MyApp.view.main.MainController', { extend: 'Ext.app.ViewController', alias: 'controller.main', onUserRemove: function() { console.log('user removed'); } }); |
listeners コンフィグのマージ
Ext JS 4 では、ベースクラスで宣言された listeners は、サブクラスやインスタンス コンフィグ宣言された listeners コンフィグに完全に上書きされてしまいます。Ext JS 5 では、listeners のAPIを改良し、ベースクラス、サブクラス、インスタンス間での宣言リスナーの正しいマージが出来る様になりました。この動きを見るために、次の例に目を通しましょう。
Ext.define('BaseClass', { extend: 'Ext.Component', listeners: { foo: function() { console.log('foo fired'); } } }); Ext.define('SubClass', { extend: 'BaseClass', listeners: { bar: function() { console.log('bar fired'); } } }); var instance = new SubClass({ listeners: { baz: function() { console.log('baz fired'); } } }); instance.fireEvent('foo'); instance.fireEvent('bar'); instance.fireEvent('baz'); |
Ext JS 4 では、上記の例では “baz” がアウトプットされます。しかし、Ext JS 5 では、listeners コンフィグが正しくマージされ。アウトプットは “foo bar baz” となります。これにより、クラスは必要なリスナーだけに宣言をするだけで済み、スーパークラスが既に持っているかもしれない listeners のことを意識しなくてすみます。
まとめ
我々は「宣言型リスナー」はアプリケーション内のイベントリスナーをより簡単に構成する点では、大きな進歩を遂げたと考えます。これらをアプリケーションロジックをハンドルする ViewController や、双方向でのデータバインディングのための ViewModel と組み合わせれば、より改良されたアプリケーション開発エクスペリエンスを手に入れることは間違いありません。ぜひ、試してみて、どう感じたかをフィードバックしてください。