前回は Sencha.io のユーザーサービスを利用して、認証機能を作成しました。今回はアプリのメイン機能であるタスクデータの CRUD(作成、読み込み、更新、削除)機能を作成します。
– 一覧画面
– 作成画面
– 詳細画面
– 編集画面
今回のチュートリアルの完成形は、以下の URL から利用できます。
JDI at Tutorial #2:
http://apps.kawanoshinobu.com/jdi-2
JDI は HTML5 is Ready App Contest で 1 位を獲得した Sencha Touch アプリです。アプリの概要については、チュートリアル #0 を参照して下さい。
Sencha Touch Tutorial: HTML5 is Ready App Contest 1位アプリ「JDI」を作る #0:
http://dev.ariel-networks.com/wp/archives/3270
Model for CRUD
JDI では、データは Sencha.io のストレージに保存します。Sencha.io のデータサービスでは、データはまずブラウザの LocalStorage に保存され、オンライン時に Sencha.io のストレージと同期します。そのため、オフライン時でもデータの CRUD 操作が可能です。モバイルアプリは電波の通信状況が悪い場所で利用されることが多いので、この特徴は大変ありがたいですね。
JDI の CRUD 機能は MVC パターンに則って構成されています。まずはモデルを作成しましょう。
ここでは「title」「duration」「due」「created」「updated」「completed」「deleted」の 7 つのフィールドを定義しています。そして「title」フィールドに対して、「presence(値必須)」のバリデーションを定義しています。
ここで興味深いのは、「due」「created」「updated」「completed」フィールドのデータ型に、独自に作成した型「datetime」を指定していることです。コメントを読むに、標準の date 型だと Sencha.io でうまく同期できないようで、日付を文字列に変換して扱っています。このクラスの他の記述も、その変換を行うことが目的です。将来的には改善されるはずですが、現状ではこのハックが必要のようです。。
1 2 3 4 5 6 7 8 9 10 |
Ext.data.Types.DATETIME = { convert: function(value) { // 'this' is the actual field that calls this convert method return (value === undefined || value === null) ? (this.getAllowNull() ? null : '') : String(value); // *文字列型に変換 }, sortType: Ext.data.SortTypes.asDate, type: 'datetime' }; |
続いて、ストアを作成します。ストアは、モデルの集合を扱うためのクラスです。名前には複数形をつけるのが慣習です。
コンフィグオプションを以下のように定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
config: { model: 'App.model.Task', // *1 storeId: 'tasks', autoLoad: true, autoSync: false, proxy: { type: 'syncstorage', // *2 id: 'jdi-tasks', // *3 owner: 'user', // *4 access: 'private' // *5 }, filters: [ // *6 { property: 'deleted', value: false } ], grouper: { // *7 sorterFn: function(item1, item2) { var groupFn = this.getGroupFn(); return App.util.Date.groupSorter( groupFn.call(this, item1), groupFn.call(this, item2)); }, groupFn: function(record) { return App.util.Date.groupName(record.get('due')); } }, sorters: [ // *8 { property: 'due' }, { property: 'duration' }, { property: 'create' }, { property: 'title' } ] }, |
利用するモデルに先ほど作成した「App.model.Task」クラスを指定します(*1)。続いて Sencha.io のデータサービスを利用するためのプロキシを定義します。type に「syncstorage」を(*2)、id にアプリのストレージ内で一意になる値を設定します(*3)。owner にはデータの所有者を指定します。ここでは「user」を指定して下さい(*4)。access では作成するデータに対するアクセス権限を指定します。private は、作成したユーザーのみがアクセスできるデータです。他に public が指定でき、こちらは同じ認証グループ内のユーザーであれば誰でもアクセスできるデータになります。タスクは個人のデータなので、ここでは private を指定します(*5)。
filters オプションは、データを読み込む際、絞り込むデータの属性を指定します。削除されたタスクは除外したいので「deleted」フィールドが false のデータのみを対象にします(*6)。grouper オプションで、タスクデータをグルーピングするための関数 groupFn とグループをソートするための関数 sorterFn を定義しています。期限日毎にグルーピングして「late」「today」「tomorrow」「soon」「someday」順にソートしています。細かな実装は util/Date.js を参照下さい。グループ内のタスクデータは「due」「duration」「create」「title」順にソートするよう指定します(*8)。
このクラスでは他に sync メソッドをオーバーライドして、Sencha.io との同期が重なってエラーになる問題を解決するためのハックをしてます。こうしてみると、Sencha.io を利用するにあたり、いろいろと苦労されたことが伺えます。。Sencha.io はまだ β 版なので仕方ないですね ^^;
broadcast メソッドでは、他のデバイスに対してデータの同期を実行しています。このメソッドは Sencha.io との同期が成功した後に実行されます。
View for CRUD
それでは、目に見える部分(View)を作りましょう。これから作成するのは「作成」「一覧」「詳細」「編集」画面です。
作成画面
Ext.form.Panel クラスを継承した入力フォームです。このフォームでは「title」「due」「duration」フィールドの値が入力できます。また、ユーザーは所要時間(duration)を分単位で入力するのですが、プログラム内では秒単位で扱うため、その変換を行っています。本来は変換処理はフォームクラスが行うのではなく、フィールドとして指定できる「duration」クラスを独自に作って、そのクラス自身が行うべき(クラスの内部で隠蔽するべき)、ということで **[HACK]** というコメントが残されています。
タスクの作成フォームを表示するパネルです。ここで興味深いのは、ボタンのタップイベントを一旦ビューが受け取っていて、ビューに関わる処理がこのビュー内で閉じていることです。具体的にはタスクの作成が完了した後のフォームの初期化やパネルの非表示はコントローラではなく、パネル自身が制御しています。コントローラに委譲したい処理に関しては、カスタムイベントを発火して通知しています。
一覧画面
一覧は「ヘッダー(グルーピングしたデータの種類を示すラベル)」と「項目」の 2 種類の行で構成されます。TaskListitem.js では、これらの表示を制御するために 2 つのクラスを定義しています。
App.view.TaskListHeader クラスは Ext.Component を継承したクラスで、ヘッダーのデザインに関する調整を行います。このクラスは後述する App.view.TaskListItem クラスからのみ利用されます。
App.view.TaskListItem クラスは Ext.dataview.component.ListItem を継承したクラスで、コンポーネントを組み合わせてデータを表示します。dataMap オプションを使わず、自前で子コンポーネントを配置して、データをセットすることで細かな制御を行っています。
詳細画面・編集画面
詳細画面、編集画面を内包するパネルです。ユーザーのボタン操作に応じて、画面を切り替えます。TaskCreate と同様、見た目に関する制御はコントローラではなく TaskPanel 自身が担うように設計されています。また、内包する各コンポーネントに容易にアクセスできるよう、プライベートな _refs 変数を宣言し、各コンポーネントを参照しています。
最終的に詳細パネルは、タブレットの場合はツーペインで常に表示され、スマートフォンの場合は必要に応じて開閉します。これらの制御に関わる処理もこのクラス内で記述されています。スマートフォン・タブレットの表示切替は以降のチュートリアルで改めて解説します。
与えられたデータを詳細表示するクラスです。値があるフィールドに関してのみ、項目を表示する仕様です。このクラスでもコンポーネントの参照を持つ変数を定義し、各コンポーネントへのアクセスを容易にしています。
これらのクラスを Main パネルに配置して完了ですが、その前にもう一点。
JDI では「autobox」という独自のレイアウトタイプを作成しています。これはデバイスの向きを変えた場合、reverse オプションが true であれば、縦持ちの場合は「horizontal」に、横持ちの場合は「vertical」に配置が変わるレイアウトです。次回のチュートリアルで紹介するセッション機能のために作られたレイアウトですが、メインパネルのレイアウトも「autobox」を指定しているため、今回ご紹介しました。
1 2 3 4 5 |
{ // content id: 'app-content', layout: 'autobox', // 独自に作成した「autobox」レイアウトを指定しています flex: 1, ... |
以上で View の作成は完了です。
Controller for CRUD
最後にコントローラクラスを作成します。
Task コントローラでは、作成、編集、削除、同期など、タスクデータを制御する処理が実装されています。
初期化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
init: function() { this.callParent(arguments); Ext.getStore('tasks').on({ // *1 scope: this, updaterecord: this._onUpdateRecord, deleterecords: this._onDeleteRecords }); }, launch: function() { // start monitoring the Sencha IO controller. this.getApplication().sio.on({ // *2 usermessage: '_onSIOUserMessage', scope: this }); }, |
tasks ストアの更新イベント、削除イベントに対して、イベントハンドラを設定しています(*1)。また Sencha.io から通知を受け取った際にストアを同期をするよう usermessage イベントにイベントハンドラを設定しています(*2)。
ビューへの更新依頼
Task コントローラでは currentTask というプロパティを持ち、この値が変更されたタイミングで TaskPanel にレコードをセットし、ビューの更新を依頼します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
config: { stores: ['Tasks'], currentTask: null, // *1 ... ... updateCurrentTask: function(record) { // *2 this.getTaskPanel().setRecord(record); if (record) { this._showTaskPanel(); } else { this._hideTaskPanel(); } }, ... this.setCurrentTask(null); // *3 ... this.setCurrentTask(record); // *3 ... |
config で currentTask オプションを定義しています(*1)。クラスシステムの仕組みにより、自動で currentTask オプションに対する getter/setter(getCurrentTask、setCurrentTask)が定義され、値の変更時に updateCurrentTask メソッドが実行されます。updateCurrentTask メソッドでは TaskPanel に変更されたデータをセットします(*2)。一覧の項目を選択した時や、後述する項目のスワイプでデータを更新した際に currentTask プロパティの更新が行われます(*3)。
作成画面表示
ツールバーの作成ボタンのタップイベントを受け取って TaskCreate パネルを表示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
/** * @private */ _showTaskCreate: function() { var panel = this.getTaskCreatePanel(); if (!Ext.isDefined(panel)) { panel = Ext.Viewport.add({ // *1 xtype: 'taskcreate', id: 'app-taskcreatepanel', modal: true, centered: true, width: '90%', height: '90%', maxWidth: '20em' }); } panel.show('fadeIn'); }, |
作成画面は、アプリの初期起動時には生成せず、利用時に初めて画面を生成しています(*1)。アプリの初期表示を速くするための工夫ですね。
一覧のイベントハンドリング
一覧の操作に対して、イベントハンドラを定義してます。selectionchange イベント(選択行の変更)に対して、currentTask プロパティの更新を行うイベントハンドラを設定しています。また JDI では、一覧の項目をスワイプすることで、タスクデータを更新できます。itemswipe イベントの内容に応じて以下の制御を行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/** * @private */ _onTaskListItemSwipe: function(list, index, target, record, e) { if (record.get('completed')) { if (e.direction == 'left') { this._restoreTask(record); // *1 } else { this._deleteTask(record); // *2 } } else { if (e.direction == 'right') { this._toggleTask(record, true); // *3 } } }, |
タスクが既に完了済み(completed に値が入っている)の場合、左にスワイプしたのであればタスクを未完了に戻す処理を(*1)、右にスワイプしたのであればタスクを削除する処理を実行します(*2)。未完了タスクの場合は、右にスワイプすることでタスクを完了する処理を行います(*3)。
タスクデータの追加・更新
ビュー(TaskCreate・TaskPanel)が発火するボタン操作のイベントを受け取って、タスクデータを更新します。
1 2 3 4 5 6 7 8 9 |
/** * @private * Mark as deleted the given *record*. */ _deleteTask: function(record) { record.set('deleted', true); // *1 this._sync(); return true; }, |
タスクデータの削除は物理削除ではなく、deleted フラグを更新するだけの論理削除です(*1)。
データの同期
タスクデータが更新された後、ストアの同期を行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
/** * @private * Synchronize the tasks store and notify changes to other devices. */ _sync: function() { var store = Ext.getStore('tasks'); store.sync(function(response) { if (response.r == 'ok') { store.broadcast(); // *1 } }); }, /** * @private * Called when the app receives a message from the Sencha IO API. */ _onSIOUserMessage: function(sender, message) { // *2 if (message == 'updated') { Ext.getStore('tasks').sync(function(response) { console.log('sio/ tasks re-synced:', response.r); }); } }, |
同期に成功した場合、Sencha.io の通知サービスを使って、他のデバイスに対してデータの同期を依頼します(*1)。broadcast メソッドは App.store.Tasks クラスで定義しましたね。_onSIOuserMessage メソッドは受け取り側の処理です。他のデバイスでの変更通知を受け取って、自身のストアを同期更新します(*2)。
以上で Task コントローラの主な実装は完了です。その他、ビューに関わる処理が少し記述されています。詳しくは以降のチュートリアルで解説しますが、これは最終的には phone プロファイルで表示する場合のみ利用します。具体的には TaskPanel の開閉と TaskPanel 上でスワイプした際にパネルを閉じる処理です。
全てのコードを解説することはできませんでしたが、リンク先のファイルを参考に、これまで紹介したクラスを実装してみて下さい。
Apply makeup
今回解説した機能で必要になるスタイルが記述された scss ファイルは以下です。
resources/sass/stylesheets/dark
├── views
│ ├── _taskcreate.scss
│ ├── _taskdetails.scss
│ ├── _tasklist.scss
│ └── _taskpanel.scss
└── widgets
├── _list.scss
└── _picker.scss
スタイルに関しては、最後にまとめて解説します。ここまでのアプリを動作させたい場合は、上記のリンクを参考にファイルを作成して下さい。
Conclusion
今回までの内容を反映したリポジトリを用意しました。都合上全てのコードを解説していませんので、説明が不足している箇所に関しては、このリポジトリのコードを参考にして頂ければと思います。
jdi-at-tutorial-2:
https://github.com/kawanoshinobu/jdi-at-tutorial-2
See Also:
jdi (Original):
https://github.com/simonbrunel/jdi
How to use Data Services:
http://docs.sencha.io/current/index.html#!/guide/concepts_store
最近のコメント