コントローラー
コントローラーはユーザの入力などのイベントに応答し、それを処理する要素です。 主な部分は、ユーザーのインタラクションの処理を担当しますが、それだけではなく、モデルの変更のイベントや、システムのイベントの処理などにも対応します。 Sencha フレームワークでは、Ext.app.Controller クラスがこの要素を構成します。
前回まで作ってきたアプリケーションに、コントローラーを加えて、ユーザーの操作などのイベントに対応させましょう。
Ext.app.Applicationとの関係
アプリケーションには通常いくつかのコントローラーが存在し、それぞれがアプリケーションの特定の部分を制御します。
アプリケーションで使用するコントローラーは、Application
オブジェクトのcontrollers
コンフィグに配列で指定します。
そうすると Application
は、自動的に各コントローラーをインスタンス化し、その参照を保持しますので、直接コントローラーをインスタンス化する必要はありません。
規則としては、SlView.controller.*
という名前をつけます。
製品を扱うコントローラーであれば、SlView.controller.Products
となります。
stores/models/views
コントローラーの
stores/models/views
の各コンフィグでは、コントローラーで使用する、ストア・モデル・ビューを定義します。
これらのコンフィグで指定されたクラスは、自動的に読み込まれます。
また、それぞれについてゲッターメソッドが定義されます。
stores
で定義されたストアは、ロードされインスタンス化されますが、モデルやビューはクラス定義がロードされるだけで、インスタンスは作られません。
refs コンフィグ (コンポーネントの参照)
refs
は
ComponentQuery
の文法を使って、ページ内のコンポーネントを簡単に指定できるパワフルな手法です。各コントローラーそれぞれに refs
を定義することができます。
refs
コンフィグは、Ext JS 4 と Sencha Touch 2 で記述方法に違いがあります。
Ext JS 4 の場合
ref
はオブジェクトの配列で、各要素は、次のようなプロパティを持つオブジェクトです。
ref
– 参照に使われる名前selector
– コンポーネントを見つけるための、ComponentQuery
のセレクター式
ref
の値に指定した名前をベースにしたgetterメソッドが自動的に作成されます。
refs: [{ ref: 'nav', selector: '#mainNav' }], |
Sencha Touch 2 の場合
refs
は、オブジェクトで、そのキーが参照に使われる名前となり、値が ComponentQuery
のセレクター式となります。
refs: { 'nav': '#mainNav' }, |
主な ComponentQuery 式
- xtype: “
panel
“または”.panel
“のようにCSSクラス的に指定します - itemId/id: “
#mypanel
“のようにidの前に”#”を付加して指定します - 子: “
panel grid
“のようにスペースでつなげて親子関係を指定します - プロパティ: “
component[autoScroll]
“や”panel[title="Test"]
“のようにプロパティの指定も可能です - メソッド: “
{isDisabled()}
“のようにメソッドの戻り値で真の場合に選択するような指定も可能です
次の例は、Ext JS 4 での例で、mainNav
というIDのコンポーネントを検索するnav
というref
を定義しています。
そして、getNav
メソッドを使って、そこにボタンを追加しています。
Ext.define('SlView.controller.Main', { extend: 'Ext.app.Controller', refs: [{ ref: 'nav', selector: '#mainNav' }], addLogoutButton: function() { this.getNav().add({ text: 'Logout' }); } }); |
この例では、addLogoutButton
というメソッドの中で、生成されたgetNav
メソッドを使っています。
このgetterメソッドの名前は、refs
に基づいて生成され、ref
プロパティ (Touch の場合はプロパティのキー) の先頭を大文字にしたものの前にget
をつけたものになります。
この場合では、getNav
はtoolbar
の参照を返すので、
そのtoolbar
にログアウトボタンを追加しています。
ここで前提としているコンポーネントは次のようなものです。
Ext.create('Ext.Toolbar', { id: 'mainNav', items: [ { text: 'Some Button' } ] }); |
addLogoutButton
メソッドが実行されると、
Some Button の後に二つ目のボタンが追加されます。
高度な refs
refs
の各オブジェクトには、オプションのプロパティがあります。
autoCreate
– trueにするとそのコンポーネントが存在しない場合に自動的に生成されます。xtype
– 自動生成するコンポーネントのxtype
を指定します。
// Ext JS 4 の場合 refs: [{ ref: 'foo', selector: '#foo', autoCreate: true, xtype: 'MyPanel' }], // Sencha Touch 2 の場合 refs: { foo: { selector: '#foo', autoCreate: true, xtype: 'MyPanel' } }, |
control メソッド / control コンフィグ
control
メソッド (Ext JS 4) 、control
コンフィグ (Sencha Touch 2) は、コンポーネントで発生したイベントをリッスンして何らかの処理をするための、リスナーを登録します。
Ext JS 4 の場合
Ext JS 4 の場合は、コントローラーの init
メソッドの中で、control
メソッドを呼び出すことで、リスナーをセットします。
control
メソッドに渡す引数は一つのオブジェクトリテラルで、キーでコンポーネントを特定して、値部分にそのコンポーネントのリスナー設定を記述したオブジェクトを指定します。
オブジェクトのキーでコンポーネントを特定しますが、ここには ComponentQuery
式が指定できます。
オブジェクトの値には、オブジェクトリテラルで、そのコントロールのリスナーを記述します。
コントロールのリスナーは、キーがイベント名、値がそのイベントに割り当てられるメソッドです。
通常メソッド自身は、コントローラーに別途定義します。
記述すると次の様になります。
Ext.define('SlView.controller.Users', { init: function() { this.control({ 'useredit button[action=save]': { click: this.updateUser } }); }, updateUser: function(button) { console.log('clicked the Save button'); } }); |
Sencha Touch 2 の場合
Sencha Touch 2 の場合は、メソッドではなく control
コンフィグでリスナーを指定します。
コントロールコンフィグにはオブジェクトを指定します。
オブジェクトのキーはコンポーネントを指定しますが、ComponentQuery
式あるいは、refs
で定義したその参照名を指定できます。
オブジェクトの値は、オブジェクトでキーにイベント名、値がイベントリスナーの関数名を文字列で指定します。
Ext.define('SlView.controller.Users', { config: { refs: { saveButton: 'useredit button[action=save]' }, control: { // refsでの定義名を指定できる saveButton: 'updateUser', // ComponentQuery 式も指定できる 'mypanel button#foo': 'onFooClick' } }, updateUser: function(button) { console.log('clicked the Save button'); }, onFooClick: function(button) { // } }); |
Ext JS 4 と Sencha Touch 2 では記述方法が異なりますのでご注意下さい。
コントローラーの作成
前回までのサンプルに動きを加えて行きます。
グリッドのダブルクリック
プロジェクトにコントローラーを追加して、グリッドの行がダブルクリックされたら、フォームにその内容を表示するようにしましょう。
app/controller/Grid.js
Ext.define('SlView.controller.Grid', { extend: 'Ext.app.Controller', refs: [ {ref: 'myForm', selector: 'myform'} ], init: function() { var me = this; me.control({ 'mygrid': { itemdblclick: me.onGridItemDblClick } }); }, onGridItemDblClick: function(view, record) { var me = this, form = me.getMyForm(); form.expand(); // この1行だけでフォームに値がセットできます form.getForm().loadRecord(record); } }); |
app/Application.js
作ったコントローラーを Application に追加します。
controllers: [ 'Main', 'Grid' ], |
これをブラウザで表示してみると、グリッドがダブルクリックされたら、フォームが開いて、フォームに値がセットされるでしょう。
フォームでのデータ編集
フォームでデータを編集できるようにしましょう。
まず、フォームビューでは、ボタンのイベントをリッスンして、カスタムイベントを発火するようにします。 こうすることで、「foo ボタンが押された」という具体的なイベントから「保存したいと言っている」という抽象的なイベントに変換できます。 そして、UIが変わってユーザーの他の動作で保存をさせたくなった場合でも、コントローラーを修正せずに済みます。
Form.js の buttons コンフィグの部分を次の様に書き換えて下さい。
app/view/Form.js
buttons: [{ text: '保存', formBind: true, handler: function(button) { var form = button.up('myform'); form.fireEvent('saveCommand', form, form.getRecord()); form.collapse(); } }, { text: 'キャンセル', handler: function(button) { var form = button.up('myform'); form.collapse(); } }] |
コントローラー側では、そのカスタムイベントをリッスンして、レコードにフォームのデータをセットします。
app/controller/Form.js
Ext.define('SlView.controller.Form', { extend: 'Ext.app.Controller', init: function() { var me = this; me.control({ 'myform': { saveCommand: me.doSave } }); }, doSave: function(form, record) { var me = this; // これだけで変更をレコードに反映できます form.updateRecord(); } }); |
このままだと空のフォームを開いてデータを保存できてしまうので、south リージョンのパネルのコンフィグに次のものを追加して、折りたたみ時にヘッダーを出さないようにします。
app/view/Main.js
collapseMode: 'mini', |
作ったコントローラーを追加します。
Application.js
controllers: [ 'Main', 'Grid', 'Form' ], |
ブラウザをリロードして動作を確認してみて下さい。フォームでの変更がグリッドに反映されるのがわかると思います。
データの追加と削除
更新ができるようになりましたので、データの追加と削除もできるようにしましょう。
まずは、グリッドのツールバーに、追加・削除ボタンを追加します。
次のコードを、title
コンフィグの次ぐらいに挿入します。
冗長に書くと、dockedItems
コンフィグに xtype: 'toolbar'
のコンポーネントを追加するのですが、簡単に書ける tbar
というコンフィグがありますので、ここではそれを利用しています。
app/view/Grid.js
tbar: [{ text: '追加', itemId: 'addButton' }, { text: '削除', itemId: 'deleteButton' }], |
フォームでは、データの更新の他、新規追加も発生することになるので、モードを保持するコンフィグオプションを追加します。 それと同時に保存ボタンのオリジナルイベントで、そのモードも送信するように変更します。
まず、フォームのビューに config
を追加し、モードを保存するプロパティを作ります。
app/view/Form.js
config:{ addMode: false }, |
buttons
コンフィグの handler
メソッドの中身を少し書き換えて、イベントの引数にモードもつけるようにします。
app/view/Form.js
buttons: [{ text: '保存', formBind: true, handler: function(button) { var form = button.up('myform'); // モードもつけて発火 form.fireEvent('saveCommand', form, form.getRecord(), form.getAddMode()); form.collapse(); } }, { |
次にグリッドのコントローラーで、イベントをハンドリングします。
まず、グリッドの参照をするので、refs
に mygrid
への参照を記述します。
app/controller/Grid.js
refs: [ {ref: 'myForm', selector: 'myform'}, {ref: 'myGrid', selector: 'mygrid'} ], |
モデルとストアのgetterメソッドを使うために、
models
コンフィグと stores
コンフィグを追加します。
app/controller/Grid.js
stores: ['Employees'], models: ['Employee'], |
control
メソッドにイベントリスナーを追加します。
app/controller/Grid.js
me.control({ : : 'button#addButton': { click: me.onAddButtonClick }, 'button#deleteButton': { click: me.onDeleteButtonClick } }); |
次にそれぞれのイベントリスナーを記述します。まず、レコードの追加では、新しいレコードのインスタンスを作って、それをフォームにセットします。
app/controller/Grid.js
onAddButtonClick: function(button) { var me = this, form = me.getMyForm(), newRec = me.getEmployeeModel().create(); form.setAddMode(true); form.expand(); form.getForm().loadRecord(newRec); }, |
レコードの削除では、現在選択されているレコードを削除します。
app/controller/Grid.js
onDeleteButtonClick: function(button) { var me = this, records = me.getMyGrid().getSelectionModel().getSelection(), store = me.getEmployeesStore(); Ext.iterate( records, function(record) { store.remove(record); }); } |
行のダブルクリックのイベントリスナーを変更して、モードを設定するようにします。
app/controller/Grid.js
onGridItemDblClick: function(view, record) { var me = this, form = me.getMyForm(); form.expand(); form.setAddMode(false); // この行を追加 form.getForm().loadRecord(record); }, |
次に、フォームのコントローラーの保存イベントのリスナーを新規追加に対応させます。
ストアのgetterを使いたいので、stores
コンフィグを指定します。
これにより、getEmployeesStore メソッドが使えるようになります。
app/controller/Form.js
stores: ['Employees'], |
保存のイベントハンドラーでは、追加モードの場合には、ストアにレコードを追加します。
app/controller/Form.js
doSave: function(form, record, addMode) { var me = this, store = me.getEmployeesStore(); record.set(form.getValues()); if( addMode ) { store.add(record); } } |
変更箇所がわりと多かったですが、これらの変更をして、追加/削除/変更の作業をしてみて下さい。
このようにビューのイベントに対して、コントローラーにリスナーを記述していきます。
コントローラー間の通信
コントローラーが別のコントローラーと通信をする方法を考えてみましょう。
コントローラーには、getController()
というメソッドがあり、それを使って他のコントローラーを参照できます。
var anotherController = this.getController('someController'); |
しかし基本的に、コントローラーのインスタンスを取得して、そのメソッドをコールするというやり方は、コントローラー同士の結合が強くなってしまいます。(他のコントローラーがないと、そのコントローラーは動作しない)
そこでコントローラー間で通信を実現するにはイベントを使います。 コントローラー A でイベントを発火し、コントローラー B でそのイベントをリッスンします。
イベントを発火させるオブジェクトは、関連するビューにあるコンポーネントでもいいですし、Application を使うこともできます。
// コンポーネントで発火 this.getMyForm().fireEvent('newEvent'); // アプリケーションで発火 this.getApplication().fireEvent('newEvent'); |
イベントをリッスンする側のコントローラーでは、リスナーを設定します。
// コンポーネントで発火させた場合 init: function() [ this.control({ 'myForm': { newEvent: 'onNewEvent' } }); } |
Application でイベントを発火させた場合は、init
メソッドでApplicationのon
メソッドなどでリスナーをセットします。
init: function () { this.getApplication().on('newEvent', this.onNewEvent); } |
今回はコントローラーについて解説しました。次回はモデル/ストアをサーバーのデータソースと接続する方法をご紹介します。