Sencha Touch開發(fā)實例:記事本應用(二)
在《Sencha Touch開發(fā)實例:記事本應用(一)》中, 我們介紹了移動跨平臺開發(fā)框架Sencha Touch的基本特性,并開始指導大家如何使用Sencha Touch開發(fā)一個簡單的記事應用,其中講解了記事頁面列表的界面開發(fā)和代碼。在本文中,將繼續(xù)講解如何完善這個記事應用中的記事列表界面的功能。在本文中,期望在學習完后,將會實現(xiàn)第一講中如下的界面框架,如下圖所示:
界面框架
下面我們分步來進行開發(fā)。
在Sencha Touch中創(chuàng)建數(shù)據(jù)模型
在創(chuàng)建記事列表前,必須先創(chuàng)建記事的數(shù)據(jù)模型,這個可以使用Sencha Touch中的Ext.regModel()方法實現(xiàn),代碼如下:
- Ext.regModel('Note', {
- idProperty: 'id',
- fields: [
- { name: 'id', type: 'int' },
- { name: 'date', type: 'date', dateFormat: 'c' },
- { name: 'title', type: 'string' },
- { name: 'narrative', type: 'string' }
- ],
- validations: [
- { type: 'presence', field: 'id' },
- { type: 'presence', field: 'title' }
- ]
- });
在記事的數(shù)據(jù)模型中,這里定義了其名稱為”Note”,idPropoerty屬性則指定了數(shù)據(jù)模型的編號列,fileds屬性是個集合,其中指定了記事這個實體的四個屬性,并且用type指定了它們的類型。注意在數(shù)據(jù)模型中,validations則指定了校驗的規(guī)則,這里指定了id和title兩個屬性是必須填寫的,在稍后的新增記事的界面中,則會看到校驗規(guī)則是如何起作用的。
要注意的是,Sencha Touch中的數(shù)據(jù)模型,可以象Hibernate一樣,可以跟其他創(chuàng)建的更多的實體模型構(gòu)成關聯(lián)關系,比如一對一,一對多等,由于在本文中不存在這樣的關系,所以我們并沒有演示,但強烈建議讀者閱讀Sencha Touch的文檔中的相關部分。
使用HTML 5本地存儲機制保存用戶本地的數(shù)據(jù)
我們需要將數(shù)據(jù)存放起來,而Ext.regStore()可以很好地創(chuàng)建數(shù)據(jù)本地存儲,將數(shù)據(jù)保存起來,代碼如下:
- Ext.regStore('NotesStore', {
- model: 'Note',
- sorters: [{
- property: 'date',
- direction: 'DESC'
- }],
- proxy: {
- type: 'localstorage',
- id: 'notes-app-localstore'
- }
- });
其中,我們通過model屬性,指定了要保存的實體為剛建立的Note,并使用sorters指定了存儲的數(shù)據(jù)中,要根據(jù)date日期字段進行倒序排列。在proxy屬性中,實際上是生成了Ext.data.LocalStorageProxy的一個實例。Ext.data.LocalStorageProxy(可參考:http://dev.sencha.com/deploy/touch/docs/?class=Ext.data.LocalStorageProxy),實際上包裝了HTML5中新的本地存儲機制API,可以在客戶端的瀏覽器中保存數(shù)據(jù),當然保存的數(shù)據(jù)不可能太復雜,Ext.data.LocalStorageProxy能負責對這些數(shù)據(jù)進行序列化和反序列化。
建立記事列表
既然數(shù)據(jù)和存儲的模型都準備好了,下面我們可以開始著手編寫記事列表的代碼了,代碼如下,很簡單:
- NotesApp.views.notesList = new Ext.List({
- id: 'notesList',
- store: 'NotesStore',
- itemTpl: '
- <div class="list-item-title">{title}</div>
- ' +
- '
- <div class="list-item-narrative">{narrative}</div>
- '
- });
在noteList列表中,我們使用的是Ext中的list列表控件,其中的store屬性指定了剛才建立好的NoteStore,而顯示模版屬性itemTpl,則分別用HTML代碼設定了title和narrative兩者的標簽,其中都應用了如下的CSS樣式:
- .list-item-title
- {
- float:left;
- width:100%;
- font-size:90%;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- .list-item-narrative
- {
- float:left;
- width:100%;
- color:#666666;
- font-size:80%;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- .x-item-selected .list-item-title
- {
- color:#ffffff;
- }
- .x-item-selected .list-item-narrative
- {
- color:#ffffff;
- }
現(xiàn)在,我們再把這個list添加到之前寫好的面板中去,如下代碼所示:
- NotesApp.views.notesListContainer = new Ext.Panel({
- id: 'notesListContainer',
- layout: 'fit',
- html: 'This is the notes list container',
- dockedItems: [NotesApp.views.notesListToolbar],
- items: [NotesApp.views.notesList]
- });
這里,把NotesApp.views.notesList加進items項中了。我們?yōu)榱诉\行能看到效果,要先往數(shù)據(jù)模型中添加一條數(shù)據(jù),如下代碼:
- Ext.regStore('NotesStore', {
- model: 'Note',
- sorters: [{
- property: 'date',
- direction: 'DESC'
- }],
- proxy: {
- type: 'localstorage',
- id: 'notes-app-store'
- },
- // TODO: 測試時用,測試后可以去除
- data: [
- { id: 1, date: new Date(), title: 'Test Note', narrative: 'This is simply a test note' }
- ]
- });
在模擬器中運行后,效果如下圖:
模擬器運行效果圖
現(xiàn)在,我們還差兩個按鈕需要新增進去,一個按鈕是新建記事的按鈕,另外一個是記事列表中,每一條后面的查看詳細情況的按鈕,如下圖:
記事按鈕
下面是把“New”這個按鈕增加進去的代碼:
- NotesApp.views.notesListToolbar = new Ext.Toolbar({
- id: 'notesListToolbar',
- title: 'My Notes',
- layout: 'hbox',
- items: [
- { xtype: 'spacer' },
- {
- id: 'newNoteButton',
- text: 'New',
- ui: 'action',
- handler: function () {
- // TODO: Create a blank note and make the note editor visible.
- }
- }
- ]
- });
其中,注意在工具條Toolbar中,使用了hbox的布局,這樣可以是這個按鈕總是靠在右邊,而這個按鈕的處理事件,我們這里先不進行處理,等待我們把新增記事的界面完成后,再編寫。
而對于記事本中每條記錄后的查看詳細的按鈕,可以通過新增加onItemDisclosure事件去實現(xiàn),代碼如下:
- NotesApp.views.notesList = new Ext.List({
- id: 'notesList',
- store: 'NotesStore',
- itemTpl: '
- <div class="list-item-title">{title}</div>
- ' +
- '
- <div class="list-item-narrative">{narrative}</div>
- ',
- onItemDisclosure: function (record) {
- // TODO: Render the selected note in the note editor.
- }
- });
在Sencha Touch的List控件中,每一行記錄都有onItemDisclosure事件(具體見http://dev.sencha.com/deploy/touch/docs/?class=Ext.List),在這個事件中,可以在獲得每一條在List中被點擊的記錄的具體情況,并進行處理,在稍后的學習中,我們會在這個事件中編寫代碼進行處理,以獲得被點擊記錄的情況,然后查看該記錄的具體情況。
接下來我們運行代碼,如下所示:
帶按鈕的運行效果圖#p#
新建記事頁的編寫
下面我們編寫新建記事頁的頁面,在這個頁面中,可以完成記事的新增,刪除和修改,先來看下我們要設計的頁面如下:
頁面設計圖
而我們希望實際運行的效果如下圖:
運行效果圖
首先,我們還是把頁面的面板設計出來,代碼如下:
- NotesApp.views.noteEditor = new Ext.form.FormPanel({
- id: 'noteEditor',
- items: [
- {
- xtype: 'textfield',
- name: 'title',
- label: 'Title',
- required: true
- },
- {
- xtype: 'textareafield',
- name: 'narrative',
- label: 'Narrative'
- }
- ]
- });
其中,我們用到了Sencha Touch中的最基本的面板樣式FormPanel,其中增加了一個文本框和一個文本區(qū)域輸入框,并且在Title的required屬性中,指定了標題是需要驗證的,用戶必須輸入內(nèi)容。
輸入框效果圖
接下來,為了快速先能看到運行效果,我們可以先修改主界面的items中的界面指定,代碼如下:
- NotesApp.views.viewport = new Ext.Panel({
- fullscreen: true,
- layout: 'card',
- cardAnimation: 'slide',
- items: [NotesApp.views.noteEditor]
- // 暫時注釋掉 [NotesApp.views.notesListContainer]
- });
可以看到運行效果如下:
接下來,繼續(xù)往界面中增加工具條,首先是最上方的包含HOME和SAVE的工具條,代碼如下:
- NotesApp.views.noteEditorTopToolbar = new Ext.Toolbar({
- title: 'Edit Note',
- items: [
- {
- text: 'Home',
- ui: 'back',
- handler: function () {
- // TODO: Transition to the notes list view.
- }
- },
- { xtype: 'spacer' },
- {
- text: 'Save',
- ui: 'action',
- handler: function () {
- // TODO: Save current note.
- }
- }
- ]
- });
其中,對于BACK按鈕,指定了ui:back的樣式,對于保存SAVE按鈕,使用了ui:action的樣式,這些都是Sencha Touch本身固定的樣式,效果如下:
導航欄
接下來,我們設計頁面下部,用于給用戶刪除記事的圖標,代碼如下:
- NotesApp.views.noteEditorBottomToolbar = new Ext.Toolbar({
- dock: 'bottom',
- items: [
- { xtype: 'spacer' },
- {
- iconCls: 'trash',
- iconMask: true,
- handler: function () {
- // TODO: Delete current note.
- }
- }
- ]
- });
在這里,要注意我們是如何把垃圾站的圖標放在最底部的,這里使用的是dock屬性中指定為bottom,并請注意這里是如何把垃圾站的圖標放到按鈕中去的(iconCls屬性指定了使用默認的垃圾站圖標,而iconMask則指定了圖標是在按鈕中)。效果如下圖:
垃圾站圖標
現(xiàn)在我們看下把上部及下部的工具條都添加后的代碼,如下所示:
- NotesApp.views.noteEditor = new Ext.form.FormPanel({
- id: 'noteEditor',
- items: [
- {
- xtype: 'textfield',
- name: 'title',
- label: 'Title',
- required: true
- },
- {
- xtype: 'textareafield',
- name: 'narrative',
- label: 'Narrative'
- }
- ],
- dockedItems: [
- NotesApp.views.noteEditorTopToolbar,
- NotesApp.views.noteEditorBottomToolbar
- ]
- });
運行代碼后,可以看到如下的效果:
運行效果圖
好了,現(xiàn)在我們可以開始學習,如何從記事的列表中,點查看每個記事詳細的按鈕,而切換到查看具體的記事,以及如何點新增按鈕,而切換到新增記事的頁面#p#
Sencha Touch中的頁面切換
我們先來看下,如何當用戶點“New”按鈕時,Sencha Touch如何從記事列表頁面中切換到新建記事的頁面中。代碼如下:
- NotesApp.views.notesListToolbar = new Ext.Toolbar({
- id: 'notesListToolbar',
- title: 'My Notes',
- layout: 'hbox',
- items: [
- { xtype: 'spacer' },
- {
- id: 'newNoteButton',
- text: 'New',
- ui: 'action',
- handler: function () {
- var now = new Date();
- var noteId = now.getTime();
- var note = Ext.ModelMgr.create(
- { id: noteId, date: now, title: '', narrative: '' },
- 'Note'
- );
- NotesApp.views.noteEditor.load(note);
- NotesApp.views.viewport.setActiveItem('noteEditor', {type: 'slide', direction: 'left'});
- }
- }
- ]
- });
請留意這里,我們補全了之前的新建按鈕中的handler事件中的代碼,下面逐一分析,首先先看這段代碼:
- var now = new Date();
- var noteId = now.getTime();
- var note = Ext.ModelMgr.create(
- { id: noteId, date: now, title: '', narrative: '' },
- 'Note'
- );
這里首先建立了一個空的note記事對象,其中該對象的date字段使用了當前的時間填充。
接下來,充分利用了Sencha Touch中的formpanel的load方法,該方法可以直接把用戶在前端界面輸入的內(nèi)容包裝成實體對象的對應屬性,這里用:
NotesApp.views.noteEditor.load(note)。最后,我們要設置新增頁面為可見,代碼為:
- NotesApp.views.viewport.setActiveItem('noteEditor', {type: 'slide', direction: 'left'});
通過NotesApp.views.viewport.setActiveItem方法,設置了noteEditor(新增記事頁面)為活動頁面,并且設置了出現(xiàn)的效果為slide滑動,方向為向左移動出現(xiàn)。
要記得,我們之前測試時,取消了主面板中的items中的新增記事頁面,由于現(xiàn)在我們設置了轉(zhuǎn)換,所以要重新加上,代碼如下:
- NotesApp.views.viewport = new Ext.Panel({
- fullscreen: true,
- layout: 'card',
- cardAnimation: 'slide',
- items: [
- NotesApp.views.notesListContainer,
- NotesApp.views.noteEditor
- ]
- });
運行后,當點NEW按鈕后,可以跳轉(zhuǎn)到新增記事頁面,如下圖:
界面跳轉(zhuǎn)#p#
驗證用戶的輸入
接下來,我們來看下,保存記事前,如何做用戶輸入的校驗。在當用戶按保存按鈕時,其邏輯如下:
1、如果用戶沒輸入標題,則提示用戶輸入。
2、如果是新的一條記事,將會將其放到cache中,如果已經(jīng)存在則更新。
3、最后更新記事列表。
在更新保存前,必須先進行校驗,所以我們修改之前的Notes實體的檢驗規(guī)則,如下代碼:
- Ext.regModel('Note', {
- idProperty: 'id',
- fields: [
- { name: 'id', type: 'int' },
- { name: 'date', type: 'date', dateFormat: 'c' },
- { name: 'title', type: 'string' },
- { name: 'narrative', type: 'string' }
- ],
- validations: [
- { type: 'presence', field: 'id' },
- { type: 'presence', field: 'title', message: 'Please enter a title for this note.' }
- ]
- });
這里,在title中,增加了message屬性,即當用戶沒輸入內(nèi)容時顯示提示的內(nèi)容。接下來,我們完善保存的代碼,如下:
- NotesApp.views.noteEditorTopToolbar = new Ext.Toolbar({
- title: 'Edit Note',
- items: [
- {
- text: 'Home',
- ui: 'back',
- handler: function () {
- NotesApp.views.viewport.setActiveItem('notesListContainer', { type: 'slide', direction: 'right' });
- }
- },
- { xtype: 'spacer' },
- {
- text: 'Save',
- ui: 'action',
- handler: function () {
- var noteEditor = NotesApp.views.noteEditor;
- var currentNote = noteEditor.getRecord();
- noteEditor.updateRecord(currentNote);
- var errors = currentNote.validate();
- if (!errors.isValid()) {
- Ext.Msg.alert('Wait!', errors.getByField('title')[0].message, Ext.emptyFn);
- return;
- }
- var notesList = NotesApp.views.notesList;
- var notesStore = notesList.getStore();
- if (notesStore.findRecord('id', currentNote.data.id) === null) {
- notesStore.add(currentNote);
- }
- notesStore.sync();
- notesStore.sort([{ property: 'date', direction: 'DESC'}]);
- notesList.refresh();
- NotesApp.views.viewport.setActiveItem('notesListContainer', { type: 'slide', direction: 'right' });
- }
- }
- ]
- });
下面分段講解代碼。首先在SAVE按鈕的handler事件中,用變量noteEditor獲得了當前新增界面NotesApp.views.noteEditor的實例,接著用getRecord()方法獲得了用戶在前端輸入,已經(jīng)裝載封裝好的Note對象,再使用updateRecord()方法,將需要更新的Note實體對象進行更新。
在調(diào)用currentNote.validate()的驗證方法時后,可以用!errors.isValid()中判斷是否校驗成功或失敗,當校驗失敗后,使用Ext.Msg.alert方法顯示用戶沒填寫記事的標題。
在通過校驗后,使用notesList.getStore()獲得當前瀏覽器中本地存儲的數(shù)據(jù)集合,并且我們判斷記錄是否存在,如果不存在,則新增,否則更新,這個很容易實現(xiàn),代碼如下:
- var notesStore = notesList.getStore();
- if (notesStore.findRecord('id', currentNote.data.id) === null) {
- notesStore.add(currentNote);
- }
這里通過判斷本地存儲中是否有currentNote.data.id,從而得知是否為新增記錄,如果沒有,則調(diào)用add方法新增。
最后,如果是更新記錄的話,調(diào)用sync方法將記錄持續(xù)化保存到本地存儲集中,再通過refresh刷新方法,刷新當前記事列表,并將頁面切換到記事列表中,如下代碼:
- notesList.refresh();
- otesApp.views.viewport.setActiveItem('notesListContainer', { type: 'slide', direction: 'right' });
這里同樣通過setActiveItem方法,切換到notesListContainer的記事列表界面。
同時,我們看下HOME按鈕的編寫,也是很簡單,如下:
- {
- text: 'Home',
- ui: 'back',
- handler: function () {
- NotesApp.views.viewport.setActiveItem('notesListContainer', { type: 'slide', direction: 'right' });'s next
- }
- }
#p#
編輯記事
我們再來看下如何編輯記事。在記事列表中,當用戶點每一條記事后的小圖標,就會直接轉(zhuǎn)換到編輯記事的頁面,顯示當前選擇記事的內(nèi)容,界面如下圖:
記事列表界面顯示
這里,我們補充完善之前的onItemDisclosure事件代碼即可,如下:
- NotesApp.views.notesList = new Ext.List({
- id: 'notesList',
- store: 'NotesStore',
- onItemDisclosure: function (record) {
- var selectedNote = record;
- NotesApp.views.noteEditor.load(selectedNote);
- NotesApp.views.viewport.setActiveItem('noteEditor', { type: 'slide', direction: 'left' });
- },
- itemTpl: '
- <div class="list-item-title">{title}</div>' +
- '<div class="list-item-narrative">{narrative}</div>',
- listeners: {
- 'render': function (thisComponent) {
- thisComponent.getStore().load();
- }
- }
- });
還記得么?onItemDisclosure事件發(fā)生在LIST列表中當用戶點每一項時,這里,我們用selectedNote變量獲得了當前的記錄,然后利用NotesApp.views.noteEditor.load方法,就可以在新增記事的頁面中,把記錄重新加載顯示出來,十分方便。
刪除記事
刪除某一個記事時,用戶點頁面底部的垃圾桶圖標即可,代碼如下:
- NotesApp.views.noteEditorBottomToolbar = new Ext.Toolbar({
- dock: 'bottom',
- items: [
- { xtype: 'spacer' },
- {
- iconCls: 'trash',
- iconMask: true,
- handler: function () {
- var currentNote = NotesApp.views.noteEditor.getRecord();
- var notesList = NotesApp.views.notesList;
- var notesStore = notesList.getStore();
- if (notesStore.findRecord('id', currentNote.data.id)) {
- notesStore.remove(currentNote);
- }
- notesStore.sync();
- notesList.refresh();
- NotesApp.views.viewport.setActiveItem('notesListContainer', { type: 'slide', direction: 'right' });
- }
- }
- ]
- });
同樣,通過currentNote變量獲得要刪除的記錄實例,然后用notesStore獲得當前的本地存儲集合,再通過findRecord方法去判斷是否存在該記錄,如果存在該記錄,則調(diào)用remove方法進行刪除,同樣,跟保存記事的代碼一樣,要記得調(diào)用sync方法同步及調(diào)用refresh方法刷新記事列表。#p#
將記事進行分組
我們這個教程中要做的最后一個例子,是按記事的日期進行分類排序,代碼如下:
- NotesApp.views.notesList = new Ext.List({
- id: 'notesList',
- store: 'NotesStore',
- grouped: true,
- emptyText: '<div style="margin: 5px;">No notes cached.</div>',
- onItemDisclosure: function (record) {
- var selectedNote = record;
- NotesApp.views.noteEditor.load(selectedNote);
- NotesApp.views.viewport.setActiveItem('noteEditor', { type: 'slide', direction: 'left' });
- },
- itemTpl: '<div class="list-item-title">{title}</div>' +
- '<div class="list-item-narrative">{narrative}</div>',
- listeners: {
- 'render': function (thisComponent) {
- thisComponent.getStore().load();
- }
- }
- });
這里,設定了grouped的屬性為true,表明列表的數(shù)據(jù)要進行分組處理,接下來,由于要根據(jù)日期進行分組,我們重寫getGroupsString方法,如下代碼:
- Ext.regStore('NotesStore', {
- model: 'Note',
- sorters: [{
- property: 'date',
- direction: 'DESC'
- }],
- proxy: {
- type: 'localstorage',
- id: 'notes-app-store'
- },
- getGroupString: function (record) {
- if (record && record.data.date) {
- return record.get('date').toDateString();
- } else {
- return '';
- }
- }
- });
在getGroupsString中,返回的是根據(jù)什么字段去進行分組,這里將每條記錄的日期轉(zhuǎn)化為字符串返回,因為我們希望每個分組的標題為日期。運行后效果如圖:
運行效果圖
本教程的完整代碼下載:
http://miamicoder.com/wp-content/uploads/2011/06/Notes-App-v1.0.zip