チュートリアル:ルーティングを使って、SPA(シングルページアプリケーション)にする(modern)
今回は、SPA対応のmodern版です。
まあ、classicとほとんど同じなのですが。。。
ルーティングの設定
メインパネルにビューコントローラを設定し、そのビューコントローラにルーティングを定義します。
/** * メインパネルクラス。 * * @class Memo.view.main.Main * @extend Ext.Panel */ Ext.define('Memo.view.main.Main', { extend: 'Ext.Panel', xtype: 'app_main', requires: [ 'Memo.view.main.ViewController' ], controller: 'app_main', layout: 'card' }); /** * メインパネルのビューコントローラクラス。 * * @class Memo.view.main.ViewController * @extend Ext.app.ViewController */ Ext.define('Memo.view.main.ViewController', { extend: 'Ext.app.ViewController', alias: 'controller.app_main', requires: [ 'Memo.view.list.Panel', 'Memo.view.regist.Panel' ], routes: { 'list': 'showList', 'regist': 'showRegist', 'regist/:id': 'showRegist' }, /** * 一覧画面を表示する。 */ showList: function () { this.switchScreen('list_panel'); }, /** * 登録画面を表示する。 * @param {Number} [id] メモID */ showRegist: function (id) { var me = this, params = {}; if (Ext.isDefined(id)) { params.id = +id; } me.switchScreen('regist_panel', params); }, /** * 画面を切り替える。 * * @param {String} screenXType 画面のxtype * @param {Object} [params] パラメータ */ switchScreen: function (screenXType, params) { var me = this, view = me.getView(), screen; params = params || {}; // 画面の存在チェック screen = view.down(screenXType); if (!screen) { // 画面を生成 screen = Ext.widget(screenXType); view.add(screen); } view.setActiveItem(screen); screen.fireEvent('showscreen', params); } });
説明を忘れていましたが、Memo.view.main.Mainは「メインパネル」としています。ビューポートじゃないのか?というと、ビューポートではありません。modernでは、自動的にビューポートが作成されており、Ext.Viewportでグローバルに参照できるようになっています。Memo.view.main.Mainは、ビューポートに自動的に配置されるビューで、大枠のビューとして使うことにしています。ちなみに、この自動生成はapp.jsというファイルに定義されています。
さて、ルーティングについてですが、これはclassicの時とほぼ同じです。異なる箇所ですが、classicではview.getLayout().setActiveItem(screen)だったコードが、view.setActiveItem(screen)となっています。こういうのがclassicとmodernでのAPIの微妙な違いによるものです。
プロキシを変更
localstorageに変更しておきます。
/** * メモモデルクラス。 * * @class Memo.model.Memo * @extend Ext.data.Model */ Ext.define('Memo.model.Memo', { extend: 'Ext.data.Model', fields: [ { name: 'id', type: 'int' }, { name: 'title', type: 'string' }, { name: 'body', type: 'string' } ], validators: { title: 'presence' } }); /** * メモストアクラス。 * * @class Memo.store.Memo * @extend Ext.data.Store */ Ext.define('Memo.store.Memo', { extend: 'Ext.data.Store', requires: [ 'Memo.model.Memo' ], model: 'Memo.model.Memo', proxy: { type: 'localstorage', id: 'memo' } });
一覧画面のビューコントローラ作成
ビューコントローラを追加し、一覧画面の処理を実装します。
/** * メモ一覧パネルクラス。 * * @class Memo.view.list.Panel * @extend Ext.Panel */ Ext.define('Memo.view.list.Panel', { extend: 'Ext.Panel', xtype: 'list_panel', requires: [ 'Memo.view.list.List', 'Memo.view.list.ViewController' ], controller: 'list', title: 'メモ一覧', tools: [ { xtype: 'button', iconCls: 'x-fa fa-plus', ui: 'action', handler: 'onTapCreateButton' } ], items: { xtype: 'list_list' }, listeners: { showscreen: 'onShowScreen' } }); /** * メモ一覧リストクラス。 * * @class Memo.view.list.List * @extend Ext.List */ Ext.define('Memo.view.list.List', { extend: 'Ext.List', xtype: 'list_list', cls: 'list-list', itemTpl: [ '<h2 class="title">{title}</h2>', '<p class="body">{body}</p>' ], store: 'Memo', listeners: { itemtap: 'onItemTap' } }); /** * メモ一覧ビューコントローラクラス。 * * @class Memo.view.list.ViewController * @extend Ext.app.ViewController */ Ext.define('Memo.view.list.ViewController', { extend: 'Ext.app.ViewController', alias: 'controller.list', /** * showscreenイベント時の処理。 */ onShowScreen: function () { Ext.getStore('Memo').load(); }, /** * 新規作成ボタンタップ時の処理。 */ onTapCreateButton: function () { this.redirectTo('regist', true); }, /** * リストitemtapイベント時の処理。 * * @param {Memo.view.list.List} list リスト * @param {Number} index インデックス番号 * @param {Ext.dom.Element} target タップ要素 * @param {Memo.model.Memo} record メモモデル */ onItemTap: function (list, index, target, record) { this.redirectTo('regist/' + record.getId(), true); } });
Memo.view.list.Listのxtypeが残念なことになっていますが、まあ、これで進めましょうw
classicで「click」と名の付いたイベントは、modernでは「tap」となります。Ext.Listで行をタップしたときは、itemtapイベントが発生するので、イベントハンドラの割り当てもitemtapに行います。
ボタンの場合は、classicと同じようにhandlerコンフィグにイベントハンドラを割り当てられますが、内部的にはtapイベント発生時に処理されています。
登録画面のビューコントローラ、ビューモデル作成
/** * メモ登録フォームパネルクラス。 * * @class Memo.view.regist.Panel * @extend Ext.form.Panel */ Ext.define('Memo.view.regist.Panel', { extend: 'Ext.form.Panel', xtype: 'regist_panel', requires: [ 'Memo.view.regist.ViewController', 'Memo.view.regist.ViewModel' ], controller: 'regist', viewModel: 'regist', title: 'メモ登録', bind: { title: 'メモ登録({labelFormMode})', record: '{record}' }, listeners: { showscreen: 'onShowScreen' }, tools: [ { xtype: 'button', ui: 'action', iconCls: 'x-fa fa-check', handler: 'onTapSaveButton' } ], bodyPadding: 10, items: [ { xtype: 'textfield', name: 'title', placeHolder: 'タイトルを入力してください' }, { xtype: 'textareafield', name: 'body', placeHolder: '本文を入力してください' } ] }); /** * メモ登録のビューコントローラクラス。 * * @class Memo.view.regist.ViewController * @extend Ext.app.ViewController */ Ext.define('Memo.view.regist.ViewController', { extend: 'Ext.app.ViewController', alias: 'controller.regist', /** * showscreenイベント時の処理。 * @param {Object} params パラメータ */ onShowScreen: function (params) { var me = this, viewModel = me.getViewModel(), store = Ext.getStore('Memo'), record = null; if (Ext.isNumber(params.id)) { record = store.getById(params.id); } if (!record) { record = Ext.create('Memo.model.Memo'); } viewModel.set('record', record); }, /** * 保存ボタンタップ時の処理。 */ onTapSaveButton: function () { var me = this, store = Ext.getStore('Memo'), form = me.getView(), record = form.getRecord(), validation; record.set(form.getValues()); validation = record.getValidation(); if (!validation.dirty) { if (record.phantom) { // 新規 store.add(record); } store.sync(); Ext.Msg.alert('完了', '保存しました', function () { me.redirectTo('list', true); }); } else { var errorMsg = []; Ext.Object.each(validation.data, function (key, value) { if (value !== true) { errorMsg.push(key + ':' + value); } }); if (errorMsg.length > 0) { Ext.Msg.alert('エラー', errorMsg.join('<br>')); } } } }); /** * メモ登録のビューモデルクラス。 * * @class Memo.view.regist.ViewModel * @extend Ext.app.ViewModel */ Ext.define('Memo.view.regist.ViewModel', { extend: 'Ext.app.ViewModel', alias: 'viewmodel.regist', data: { /** * @cfg {Memo.model.Memo} 選択中のメモモデル。 */ record: null }, formulas: { /** * フォームのモード(新規か編集か)のラベルを返す。 * * @param {Function} get * @returns {string} フォームのモード(新規か編集か)のラベル */ labelFormMode: function (get) { var record = get('record'); return record ? '編集' : '新規作成'; } } });
大きく異なる部分は、入力チェックの部分です。
classicでは入力フィールドにバリデーションチェックの定義を設定しますが、modernではモデルのvalidatorsにバリデーションチェックの定義を設定します。
具体的には、モデルのgetValidationの戻り値であるExt.data.Validationのdirtyでエラーがあるかどうかを判定します。
そして、エラーがあった場合の表示方法が乏しいのがmodernの特徴です(ノД`)
classicの場合、エラーのあった入力フィールドにエラーメッセージを表示する機能が標準で備わっていますが、modernでは無いためユーザ自身で実装する必要があります。
先のコードでは、メッセージボックスで簡単にエラー表示しています。
defaultTokenを設定しておく
ここはclassicと同じです。
/** * アプリケーションクラス。 * * @class Memo.Application * @extend Ext.app.Application */ Ext.define('Memo.Application', { extend: 'Ext.app.Application', name: 'Memo', stores: [ 'Memo' ], defaultToken : 'list' });
最終的に、↓のようになりました。
エラーメッセージがひどい状態ですが、今回はここまで。
次回は削除できるようにします。