在上一篇文章中(http://blog.youkuaiyun.com/stoneson/article/details/6870660),我们介绍了移动跨平台开发框架Sencha Touch的基本特性,并开始指导大家如何使用Sencha Touch开发一个简单的记事应用,其中讲解了记事页面列表的界面开发和代码。在本文中,将继续讲解如何完善这个记事应用中的记事列表界面的功能。在本文中,期望在学习完后,将会实现第一讲中如下的界面框架,如下图所示:
▲界面框架
下面我们分步来进行开发。
在Sencha Touch中创建数据模型
在创建记事列表前,必须先创建记事的数据模型,这个可以使用Sencha Touch中的Ext.regModel()方法实现,代码如下:
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' }
]
});
在记事的数据模型中,这里定义了其名称为”Note”,idPropoerty属性则指定了数据模型的编号列,fileds属性是个集合,其中指定了记事这个实体的四个属性,并且用type指定了它们的类型。注意在数据模型中,validations则指定了校验的规则,这里指定了id和title两个属性是必须填写的,在稍后的新增记事的界面中,则会看到校验规则是如何起作用的。
要注意的是,Sencha Touch中的数据模型,可以象Hibernate一样,可以跟其他创建的更多的实体模型构成关联关系,比如一对一,一对多等,由于在本文中不存在这样的关系,所以我们并没有演示,但强烈建议读者阅读Sencha Touch的文档中的相关部分。
使用HTML 5本地存储机制保存用户本地的数据
我们需要将数据存放起来,而Ext.regStore()可以很好地创建数据本地存储,将数据保存起来,代码如下:
model: ' Note',
sorters: [{
property : ' date',
direction: ' DESC'
}],
proxy: {
type: ' localstorage',
id: ' notes-app-localstore'
}
});
其中,我们通过model属性,指定了要保存的实体为刚建立的Note,并使用sorters指定了存储的数据中,要根据date日期字段进行倒序排列。在proxy属性中,实际上是生成了Ext.data.LocalStorageProxy的一个实例。Ext.data.LocalStorageProxy(可参考:
http://dev.sencha.com/deploy/touch/docs/?class=Ext.data.LocalStorageProxy),实际上包装了HTML5中新的本地存储机制API,可以在客户端的浏览器中保存数据,当然保存的数据不可能太复杂,Ext.data.LocalStorageProxy能负责对这些数据进行序列化和反序列化。
建立记事列表
既然数据和存储的模型都准备好了,下面我们可以开始着手编写记事列表的代码了,代码如下,很简单:
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样式:
{
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;
}
现在,我们再把这个list添加到之前写好的面板中去,如下代码所示:
id: ' notesListContainer',
layout: ' fit',
html: ' This is the notes list container',
dockedItems: [NotesApp.views.notesListToolbar],
items: [NotesApp.views.notesList]
});
这里,把NotesApp.views.notesList加进items项中了。我们为了运行能看到效果,要先往数据模型中添加一条数据,如下代码:
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' }
]
});
在模拟器中运行后,效果如下图:
▲模拟器运行效果图
现在,我们还差两个按钮需要新增进去,一个按钮是新建记事的按钮,另外一个是记事列表中,每一条后面的查看详细情况的按钮,如下图:
▲记事按钮
下面是把“New”这个按钮增加进去的代码:
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事件去实现,代码如下:
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中被点击的记录的具体情况,并进行处理,在稍后的学习中,我们会在这个事件中编写代码进行处理,以获得被点击记录的情况,然后查看该记录的具体情况。
接下来我们运行代码,如下所示:
▲带按钮的运行效果图
新建记事页的编写
下面我们编写新建记事页的页面,在这个页面中,可以完成记事的新增,删除和修改,先来看下我们要设计的页面如下:
▲页面设计图
而我们希望实际运行的效果如下图:
▲运行效果图
首先,我们还是把页面的面板设计出来,代码如下:
id: ' noteEditor',
items: [
{
xtype: ' textfield',
name: ' title',
label: ' Title',
required: true
},
{
xtype: ' textareafield',
name: ' narrative',
label: ' Narrative'
}
]
});
其中,我们用到了Sencha Touch中的最基本的面板样式FormPanel,其中增加了一个文本框和一个文本区域输入框,并且在Title的required属性中,指定了标题是需要验证的,用户必须输入内容。
▲输入框效果图
接下来,为了快速先能看到运行效果,我们可以先修改主界面的items中的界面指定,代码如下:
fullscreen: true ,
layout: ' card',
cardAnimation: ' slide',
items: [NotesApp.views.noteEditor]
// 暂时注释掉 [NotesApp.views.notesListContainer]
});
可以看到运行效果如下:
接下来,继续往界面中增加工具条,首先是最上方的包含HOME和SAVE的工具条,代码如下:
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本身固定的样式,效果如下:
▲导航栏
接下来,我们设计页面下部,用于给用户删除记事的图标,代码如下:
dock: ' bottom',
items: [
{ xtype: ' spacer' },
{
iconCls: ' trash',
iconMask: true ,
handler: function () {
// TODO: Delete current note.
}
}
]
});
在这里,要注意我们是如何把垃圾站的图标放在最底部的,这里使用的是dock属性中指定为bottom,并请注意这里是如何把垃圾站的图标放到按钮中去的(iconCls属性指定了使用默认的垃圾站图标,而iconMask则指定了图标是在按钮中)。效果如下图:
▲垃圾站图标
现在我们看下把上部及下部的工具条都添加后的代码,如下所示:
id: ' noteEditor',
items: [
{
xtype: ' textfield',
name: ' title',
label: ' Title',
required: true
},
{
xtype: ' textareafield',
name: ' narrative',
label: ' Narrative'
}
],
dockedItems: [
NotesApp.views.noteEditorTopToolbar,
NotesApp.views.noteEditorBottomToolbar
]
});
运行代码后,可以看到如下的效果:
▲运行效果图
好了,现在我们可以开始学习,如何从记事的列表中,点查看每个记事详细的按钮,而切换到查看具体的记事,以及如何点新增按钮,而切换到新增记事的页面
Sencha Touch中的页面切换
我们先来看下,如何当用户点“New”按钮时,sencha touch如何从记事列表页面中切换到新建记事的页面中。代码如下:
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 noteId = now .getTime();
var note = Ext.ModelMgr.create(
{ id: noteId, date : now , title: ' ', narrative: '' },
' Note'
);
这里首先建立了一个空的note记事对象,其中该对象的date字段使用了当前的时间填充。
接下来,充分利用了sencha touch中的formpanel的load方法,该方法可以直接把用户在前端界面输入的内容包装成实体对象的对应属性,这里用:
NotesApp.views.noteEditor.load(note)。最后,我们要设置新增页面为可见,代码为:
NotesApp.views.viewport.setActiveItem('noteEditor', {type: 'slide', direction: 'left'});
通过NotesApp.views.viewport.setActiveItem方法,设置了noteEditor(新增记事页面)为活动页面,并且设置了出现的效果为slide滑动,方向为向左移动出现。
要记得,我们之前测试时,取消了主面板中的items中的新增记事页面,由于现在我们设置了转换,所以要重新加上,代码如下:
fullscreen: true ,
layout: ' card',
cardAnimation: ' slide',
items: [
NotesApp.views.notesListContainer,
NotesApp.views.noteEditor
]
});
运行后,当点NEW按钮后,可以跳转到新增记事页面,如下图:
▲界面跳转
验证用户的输入
接下来,我们来看下,保存记事前,如何做用户输入的校验。在当用户按保存按钮时,其逻辑如下:
1、如果用户没输入标题,则提示用户输入。
2、如果是新的一条记事,将会将其放到cache中,如果已经存在则更新。
3、最后更新记事列表。
在更新保存前,必须先进行校验,所以我们修改之前的Notes实体的检验规则,如下代码:
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属性,即当用户没输入内容时显示提示的内容。接下来,我们完善保存的代码,如下:
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()方法获得了用户在前端输入,已经装载封装好的Note对象,再使用updateRecord()方法,将需要更新的Note实体对象进行更新。
在调用currentNote.validate()的验证方法时后,可以用!errors.isValid()中判断是否校验成功或失败,当校验失败后,使用Ext.Msg.alert方法显示用户没填写记事的标题。
在通过校验后,使用notesList.getStore()获得当前浏览器中本地存储的数据集合,并且我们判断记录是否存在,如果不存在,则新增,否则更新,这个很容易实现,代码如下:
if (notesStore.findRecord( ' id', currentNote.data.id) === null) {
notesStore.add(currentNote);
}
这里通过判断本地存储中是否有currentNote.data.id,从而得知是否为新增记录,如果没有,则调用add方法新增。
最后,如果是更新记录的话,调用sync方法将记录持续化保存到本地存储集中,再通过refresh刷新方法,刷新当前记事列表,并将页面切换到记事列表中,如下代码:
NotesApp.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
}
}
编辑记事
我们再来看下如何编辑记事。在记事列表中,当用户点每一条记事后的小图标,就会直接转换到编辑记事的页面,显示当前选择记事的内容,界面如下图:
▲记事列表界面显示
这里,我们补充完善之前的onItemDisclosure事件代码即可,如下:
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事件发生在LIST列表中当用户点每一项时,这里,我们用selectedNote变量获得了当前的记录,然后利用NotesApp.views.noteEditor.load方法,就可以在新增记事的页面中,把记录重新加载显示出来,十分方便。
删除记事
删除某一个记事时,用户点页面底部的垃圾桶图标即可,代码如下:
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方法去判断是否存在该记录,如果存在该记录,则调用remove方法进行删除,同样,跟保存记事的代码一样,要记得调用sync方法同步及调用refresh方法刷新记事列表。
将记事进行分组
我们这个教程中要做的最后一个例子,是按记事的日期进行分类排序,代码如下:
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,表明列表的数据要进行分组处理,接下来,由于要根据日期进行分组,我们重写getGroupsString方法,如下代码:
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中,返回的是根据什么字段去进行分组,这里将每条记录的日期转化为字符串返回,因为我们希望每个分组的标题为日期。运行后效果如图:
▲运行效果图
本教程的完整代码下载:
http://miamicoder.com/wp-content/uploads/2011/06/Notes-App-v1.0.zip