モデルのアソシエーション
ExtJSのモデルは「1 対 1」「1 対 複数」といったデータモデル上の関係を持つことができます(アソシエーション機能)。
例えば、下のようなデータをモデルで取り扱う場合に使えます。
{ "id": 100, "created": "2017/03/15 15:20:25", "user": { "id": 1, "name": "山田太郎", "kana": "やまだたろう" }, "details": [ { "id": 1, "num": 1, "item": { "id": 1, "name": "ジャケット", "price": 5000 } }, { "id": 2, "num": 2, "item": { "id": 2, "name": "Tシャツ", "price": 1000 } } ] }
モデル作成
さきほどのJSONデータは注文データを表しています。
userは購入者、detailsは注文の詳細(どの商品をいくつ注文したのか)を意味しています。
それを踏まえてモデルを作成します。
/** * ユーザモデルクラス。 * * @class Sample.model.User * @extend Ext.data.Model */ Ext.define('Sample.model.User', { extend: 'Ext.data.Model', fields: [ { // ユーザID name: 'id', type: 'int' }, { // 氏名 name: 'name', type: 'string' }, { // かな name: 'kana', type: 'string' } ] }); /** * 商品モデルクラス。 * * @class Sample.model.Item * @extend Ext.data.Model */ Ext.define('Sample.model.Item', { extend: 'Ext.data.Model', fields: [ { // 商品ID name: 'id', type: 'int' }, { // 商品名 name: 'name', type: 'string' }, { // 単価 name: 'price', type: 'int' } ] }); /** * 注文詳細モデルクラス。 * * @class Sample.model.OrderDetail * @extend Ext.data.Model */ Ext.define('Sample.model.OrderDetail', { extend: 'Ext.data.Model', requires: [ 'Sample.model.Item' ], fields: [ { // 注文詳細ID name: 'id', type: 'int' }, { // 注文数 name: 'num', type: 'int' } ], hasOne: [ { // 注文モデル name: 'item', model: 'Sample.model.Item' } ] }); /** * 注文モデルクラス。 * * @class Sample.model.Order * @extend Ext.data.Model */ Ext.define('Sample.model.Order', { extend: 'Ext.data.Model', requires: [ 'Sample.model.User', 'Sample.model.OrderDetail' ], fields: [ { // 注文ID name: 'id', type: 'int' }, { // 作成日時(注文日時) name: 'created', type: 'date', dateFormat: 'Y/m/d H:i:s' } ], hasOne: [ { name: 'user', model: 'Sample.model.User' } ], hasMany: [ { name: 'details', model: 'Sample.model.OrderDetail' } ] });
hasOneとhasManyでアソシエーションを設定しています。
ひとつのデータであればhasOne、複数のデータであればhasManyで表現できます。
nameに指定する文字列は、JSONのプロパティ名と合わせます。
modelには、モデルクラス名をフルネームで指定します。
(Application.jsなどでモデルをrequiresするのをお忘れなく)
モデルにプロキシを設定
今回は非同期でデータをロードすることにしました。
モデルにExt.data.proxy.Ajaxプロキシを設定します。
/** * 注文モデルクラス。 * * @class Sample.model.Order * @extend Ext.data.Model */ Ext.define('Sample.model.Order', { extend: 'Ext.data.Model', requires: [ 'Sample.model.User', 'Sample.model.OrderDetail', 'Ext.data.proxy.Ajax', 'Ext.data.reader.Json' ], fields: [ { // 注文ID name: 'id', type: 'int' }, { // 作成日時(注文日時) name: 'created', type: 'date', dateFormat: 'Y/m/d H:i:s' } ], hasOne: [ { // ユーザ name: 'user', model: 'Sample.model.User' } ], hasMany: [ { // 注文詳細 name: 'details', model: 'Sample.model.OrderDetail' } ], proxy: { type: 'ajax', url: '/data/order.json', reader: { type: 'json', rootProperty: 'data' } } });
これでSample.model.Order.loadを実行したときに、proxyコンフィグに設定した内容でロードしてくれます。
ダミーデータを作成
ダミーデータを作成します。
プロキシのURLを/data/order.jsonとしているので、このファイルをプロジェクトに作成します。
{ "success": true, "data": { "id": 100, "created": "2017/03/15 15:20:25", "user": { "id": 1, "name": "山田太郎", "kana": "やまだたろう" }, "details": [ { "id": 1, "num": 1, "item": { "id": 1, "name": "ジャケット", "price": 5000 } }, { "id": 2, "num": 2, "item": { "id": 2, "name": "Tシャツ", "price": 1000 } } ] } }
これで http://localhost:1841/data/order.json にアクセスできるようになりました。
ロードしてみる
試しにロードしてみます。
Developer Toolsなどのコンソールから下を実行すると、ログが出力されればOKです。
Sample.model.Order.load(100, { success: function (record) { console.log(record); console.log(record.getUser()); console.log(record.details()); } });
record変数に、ロードしたデータを保持したSample.model.Orderのインスタンスが設定されます。
注文モデルからユーザモデルを参照するには自動的に定義された「getUser」メソッドを使います。
また、注文詳細は自動的に定義された「details」メソッドを使います。detailsメソッドの戻り値は注文詳細モデルのストアになっています。
(loadメソッドの第1引数には注文IDを指定しますが、レスポンスデータのidと一致しないとエラーとなるので注意してください)
ロードしたデータを画面に表示する
最後に、ロードしたデータを画面に出力してみます。
ビューモデルがゴチャゴチャしてるのが嫌な感じ。もっと良い書き方がありそう。
/** * メインパネル。 * * @class Sample.view.main.Panel * @extend Ext.panel.Panel */ Ext.define('Sample.view.main.Panel', { extend: 'Ext.panel.Panel', xtype: 'main_panel', requires: [ 'Sample.view.order.detail.Content', 'Sample.view.main.ViewController', 'Sample.view.main.ViewModel' ], controller: 'main', viewModel: 'main', layout: 'fit', items: [ { xtype: 'order_detail_content' } ], listeners: { afterrender: 'onAfterRender' } }); /** * 注文内容クラス。 * * @class Sample.view.order.detail.Content * @extend Ext.Component */ Ext.define('Sample.view.order.detail.Content', { extend: 'Ext.Component', xtype: 'order_detail_content', cls: 'order-detail-content', bind: { data: { order: '{order}', user: '{user}', orderDetails: '{orderDetails}' } }, tpl: [ '<h1>#{order.id} 注文情報</h1>', '<table>', '<tbody>', '<tr>', '<th>注文者</th>', '<td>{user.name}</td>', '</tr>', '<tr>', '<th>注文日時</th>', '<td>{[Ext.Date.format(values.order.created, "Y/m/d H:i")]}</td>', '</tr>', '</tbody>', '</table>', '<h2>注文詳細</h2>', '<table>', '<tbody>', '<tr>', '<th>商品名</th>', '<th class="right">単価</th>', '<th class="right">数量</th>', '<th class="right">小計</th>', '</tr>', '<tpl for="orderDetails">', '<tr>', '<td>{item.name}</td>', '<td class="right">{item.price}円</td>', '<td class="right">{num}</td>', '<td class="right">{[values.item.price * values.num]}円</td>', '</tr>', '</tpl>', '</tbody>', '</table>', '<div class="total">合計: {[this.getTotal(values.orderDetails)]}円</div>', { getTotal: function (orderDetails) { var total = 0; Ext.Array.each(orderDetails, function (detail) { total += detail.item.price * detail.num; }); return total; } } ] }); /** * ビューコントローラクラス。 * * @class Sample.view.main.ViewController * @extend Ext.app.ViewController */ Ext.define('Sample.view.main.ViewController', { extend: 'Ext.app.ViewController', alias: 'controller.main', /** * afterrenderイベント時の処理。 */ onAfterRender: function () { var me = this; Sample.model.Order.load(100, { success: function (record) { me.getViewModel().set('record', record); } }); } }); /** * ビューモデルクラス。 * * @class Sample.view.main.ViewModel * @extend Ext.app.ViewModel */ Ext.define('Sample.view.main.ViewModel', { extend: 'Ext.app.ViewModel', alias: 'viewmodel.main', data: { /** * @cfg {Sample.model.Order} 注文モデル。 */ record: null }, formulas: { /** * 注文情報を返す。 * * @param {Function} get * @returns {Object} 注文情報 */ order: function (get) { var record = get('record'); return record ? record.getData() : {}; }, /** * ユーザ情報を返す。 * * @param {Function} get * @returns {Object} ユーザ情報 */ user: function (get) { var record = get('record'); return record ? record.getUser().getData() : {}; }, /** * 注文詳細情報を返す。 * @param {Function} get * @returns {Array} 注文詳細情報 */ orderDetails: function (get) { var record = get('record'); if (record) { var data = []; record.details().each(function (detail) { var item = detail.getItem(); data.push(Ext.apply(detail.getData(), { item: item.getData() })); }); return data; } else { return []; } } } });
@charset "UTF-8"; .order-detail-content { background-color: #f9f9f9; padding: 20px; h1, h2 { font-weight: normal; line-height: 1.2; margin: 0 0 10px 0; } table { width: 100%; border-collapse: collapse; margin-bottom: 15px; th, td { padding: 10px; border: 1px solid #ddd; } th { background-color: #009688; color: #fff; font-weight: normal; text-align: left; } td { background-color: #fff; } } .total { font-size: 2.5em; line-height: 1.2; text-align: right; text-decoration: underline; } .right { text-align: right; } }
こんな感じになりました。
アソシエーションの機能はなかなか便利です。サーバ側のレスポンスも、あらかじめアソシエーションを考慮した形式にしておくと良いでしょう。