In this third part of this tutorial on how to create a Sencha Touch application, we are going to create the Note Editor View. This is the View that will allow our users to create, edit and delete notes.When we finish this article, our application will have the ability to create notes, and edit existing notes. Let’s get started building the Note Editor View.
在本节中我们将会创建NoteEditor视图,在这个视图中允许用户编辑,删除,创建等操作。当结束这篇文章的时候,我们的应用就应该具备了创建笔记和编辑已存笔记的功能。
Creating a Form Panel in Sencha Touch(创建一个表单面板)
We will place the View’s source code in the NoteEditor.js file, which we will create in the view directory:
在view目录中创建NoteEditor.js文件。
In the file, we will define an empty NoteEditor Class like so:
在这个文件中我们将如下定义NoteEditor类:
Ext.define("NotesApp.view.NoteEditor", { extend: "Ext.form.Panel",//继承form.Panel requires: "Ext.form.FieldSet",//导入FieldSet类 alias: "widget.noteeditor", config:{ scrollable:'vertical' }, initialize: function () { this.callParent(arguments); } });
The Note Editor is an extension of the Ext.form.Panel Class. As we will use a FieldSet Class instance in the View, we are asking the loader to download its source by using the requires config. We are also using the scrollable config to allow the contents of the Panel to scroll vertically. This stops the form from being cropped when its height is larger than the screen height of the device.
NoteEditor是Ext.form.Panel类的一个扩展。因为在视图中我们要用到FieldSet类的实例,所以使用requires请求加载这个类的源代码。我们也经常用到Scrollable配置项来允许面板可以滑动。
As we did with the Notes ListContainer Class, we are going to use the initialize() function to define the Note Editor’s components:
就像在noteListContainer视图中一样,我们通过重写initialize()函数来定义NoteEditor的视图组件。
Ext.define("NotesApp.view.NoteEditor", { extend: "Ext.form.Panel", requires: "Ext.form.FieldSet", alias: "widget.noteeditor", config:{ scrollable:'vertical' }, initialize: function () { this.callParent(arguments); var backButton = { xtype: "button", ui: "back", text: "Home" }; var saveButton = { xtype: "button", ui: "action", text: "Save" }; var topToolbar = { xtype: "toolbar", docked: "top", title: "Edit Note", items: [ backButton, { xtype: "spacer" }, saveButton ] }; var deleteButton = { xtype: "button", iconCls: "trash", iconMask: true, scope: this }; var bottomToolbar = { xtype: "toolbar", docked: "bottom", items:[ deleteButton ] }; var noteTitleEditor = { xtype: 'textfield', name: 'title', label: 'Title', required: true }; var noteNarrativeEditor = { xtype: 'textareafield', name: 'narrative', label: 'Narrative' }; this.add([ topToolbar, { xtype: "fieldset", items: [noteTitleEditor, noteNarrativeEditor] }, bottomToolbar ]); } });
Within initialize(), after invoking callParent(), we proceed to define the top Toolbar, along with its two buttons, the Home Button and the Save Button. We then define the bottom Toolbar and the Delete Button.
在initialize()函数中,调用父类构造方法之后,定义一个停靠在屏幕上方的工具栏和它上面的两个按钮(home和save)稍后我们定义
下方的工具栏以及delete按钮。
The noteTitleEditor and noteNarrativeEditor are the fields we will use to edit the note’s title and narrative. They are instances of the Ext.form.Text and Ext.form.TextArea Classes.
noteTitleEditor 和 noteNarrativeEditor 用来编辑笔记的标题和内容。他们是Ext.form.Text 类和Ext.form.TextArea 类的实例。
Once all the View’s components are defined, we proceed to add them to the View. This is where we also add an Ext.form.FieldSet instance to enhance the appearance of the form. The title and narrative editors will render within the FieldSet:
组件定义之后,我们将他们添加到视图中。在这里我们还添加了一个Ext.form.FieldSet实例来提高表单的外观效果。标题和笔记的内容描述将会和fieldset呈现在一起。
this.add([ topToolbar, { xtype: "fieldset", items: [noteTitleEditor, noteNarrativeEditor] }, bottomToolbar ]);
Rendering a View In Sencha Touch(在ST中呈现/渲染视图)
Before we start developing the features of the Note Editor, we are going to work on the code that will render this View when the New Button in the Notes List Container is tapped.
在编码实现NoteEditor页面的编辑,新建,删除等特性之前我们要实现这个功能:点击New按钮,弹出NoteEditor界面。
In the previous chapter of this tutorial we created the tap handler for the New Button, which fires the newNoteCommand event. We also added the onNewNoteCommand() listener to the Notes Controller. We will use this function to activate the Note Editor View.
To activate the Note Editor View in the Controller, we first need to acquire a reference to the View. We will use the noteeditor ref for this purpose:
在这个教程的前几节我们为New按钮创建了点击处理的机制。我们也在控制器中添加了onNewNoteCommand()监听。我们用这个函数来激活NoteEditor界面。
为了在控制器中触发NoteEditor界面,我们首先需要添加view的引用。
Ext.define("NotesApp.controller.Notes", { extend: "Ext.app.Controller", config: { refs: { // We're going to lookup our views by xtype. notesListContainer: "noteslistcontainer", noteEditor: "noteeditor" }, // Remainder of the controller’s code omitted for brevity. });
Remember, this ref automatically creates a getNoteEditor() function in the controller, which we can use to refer to the NoteEditor instance and make it the active View in the application.
记住,在控制器中这个函数将自动创建getNoteEdior()函数。我们可以通过它来引用NoteEditor的实例,并且让他成为应用的当前窗口。
Next, we need to modify the onNewNoteCommand() function:
下一步,修改onNewNoteCommand()函数:
onNewNoteCommand: function () { console.log("onNewNoteCommand"); //给note设置一个id var now = new Date(); var noteId = (now.getTime()).toString() + (this.getRandomInt(0, 100)).toString(); //创建一个新的note实例 var newNote = Ext.create("NotesApp.model.Note", { id: noteId, dateCreated: now, title: "", narrative: "" }); //激活NoteEditor按钮 this.activateNoteEditor(newNote); }
Here things get more interesting. We are creating a new note, and passing it to(传递) the activateNoteEditor() function.
We are going to use the getRamdomInt() helper function to generate the unique id for a note:
这就更有意思了,我们创建一个新的note数据模型的实例然后传递给activateNoteEdit()函数。我们将使用getRamdomInt()函数来为笔记生成唯一的id。
getRandomInt: function (min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }
The activateNoteEditor() function will load the new note into the Note Editor, and make the Editor active:
activateNoteEditor() 函数将会加载新笔记到NoteEditor,而且激活Note Editor.
activateNoteEditor: function (record) { var noteEditor = this.getNoteEditor();//前面已经refs,自动生成此方法 noteEditor.setRecord(record); // load() is deprecated. Ext.Viewport.animateActiveItem(noteEditor, this.slideLeftTransition); }
Here we are taking advantage of the Ext.form.Panel’s setRecord() function, which allows us to load the values of a model instance into the form’s fields whose names match those of the model’s fields.
This function also uses the slideLeftTransition variable, which we need to define like so:
这里我们利用Ext.form.Panel的setRecord()函数将model实例的值加载到表单的字段中,加载过程中model的字段名和form的字段名相匹配。
slideLeftTransition: { type: 'slide', direction: 'left' }
The transition(过度,过度时期) will bring the Note Editor into view with a slide(滑动) motion(运动,议案) from the right to the left.
This is all the Controller needs to do in order to activate the Note Editor View when the New Button is tapped. However, we need to make the application aware of the NoteEditor Class.
界面的转换效果是这样的:NoteEditor界面会从右向左滑到屏幕中间,覆盖noteList界面。控制器只需要做这些就能达到点击新建按钮就激活NoteEditor界面的效果。当然,我们应该让应用知道NoteEditor类。
Making The Application Aware Of A View(让应用知道你的视图)
In the app.js file, we will add the new View to the views config:
在app.js文件中我们会添加新的view(视图)引用:
views: ["NotesList", "NotesListContainer", "NoteEditor"]
We are also going to instantiate(实例化) the View in the Application’s launch function:
我们也需要在应用的入口函数中实例化视图:
Ext.application({ name: "NotesApp", models: ["Note"], stores: ["Notes"], controllers: ["Notes"], views: ["NotesList", "NotesListContainer", "NoteEditor"], launch: function () { var notesListContainer = { xtype: "noteslistcontainer" }; var noteEditor = { xtype: "noteeditor" }; Ext.Viewport.add([notesListContainer,noteEditor]); } });
Ready to check it out? Well, start the emulator.
A tap on the New Button should render the Note Editor View:
准备好了么?启动模拟器,点击new按钮,将会激活NoteEditor界面。
Editing A Record Rendered In A Sencha Touch List(在ST中编辑列表项)
We also want to activate the Note Editor View when a note’s disclosure button is tapped:
我们也希望通过列表项中的disclose按钮来激活NoteEditor界面:
In order to accomplish this, we need to revisit the onEditNoteCommand() function in the Controller, and add the code that will activate the NoteEditor:
要实现这个效果,我们需要回到控制器的onEditNoteCommand()函数中,添加如下代码实现激活NoteEditor界面:
onEditNoteCommand: function (list, record) { console.log("onEditNoteCommand"); this.activateNoteEditor(record); }
Incredibly(令人难以置信的是) simple thanks to the fact that the disclose event supplies the selected Note model instance to the handler through the record argument. All we need to do here is call activateNoteEditor(), which we created a few minutes ago.
Back in the emulator, we should be able to see the Note Editor render the selected note:
Now it is time to save the note. We need to take care of a few things in order for this to happen.
现在到了实现保存功能的时候了,要达到这个效果,我们需要注意几个事情。
Using A LocalStorage Proxy In Sencha Touch(在ST中使用本地存储代理)
First, we need to stop using hard-coded notes as the data for the Notes store.Up to this point(到此为止), we have been using the data config to define a few hard-coded records in the store:
首先我们应该停止在store中使用硬编码数据。之前我们使用data配置项配置一些硬编码数据:
Ext.define("NotesApp.store.Notes", { extend: "Ext.data.Store", config: { model: "NotesApp.model.Note", data: [ { title: "Note 1", narrative: "narrative 1" }, { title: "Note 2", narrative: "narrative 2" }, { title: "Note 3", narrative: "narrative 3" }, { title: "Note 4", narrative: "narrative 4" }, { title: "Note 5", narrative: "narrative 5" }, { title: "Note 6", narrative: "narrative 6" } ], sorters: [{ property: 'dateCreated', direction: 'DESC'}] } });
As we intend to(打算) cache notes on the device that runs the app, we are going to discontinue(终止) the data config in the store, and define a LocalStorage proxy as follows:
因为我们要在设备上缓存应用中的数据。所以我们应该终止store中的data配置,取而代之的是一个像这样的LocalStorage代理:
Ext.define("NotesApp.store.Notes", { extend: "Ext.data.Store", requires:"Ext.data.proxy.LocalStorage", config: { model: "NotesApp.model.Note", proxy: { type: 'localstorage', id: 'notes-app-store' }, sorters: [{ property: 'dateCreated', direction: 'DESC'}] } });
LocalStorageProxy uses the HTML5 localStorage API to save Model data on the client browser. This proxy is ideal(理想的) for storing(存储) multiple records of similar data. And it requires that we provide the id config, which is the key that will identify our data in the localStorage object.
Now the Notes store has the ability to save and read data from localStorage, and we can go back to the Views and Controller to take care of the functions that will save the data.
LocalStorageProxy使用HTML5的localStorageAPI来保存Model数据到设备。这个代理对于多种相似数据的存储是很理想的。它需要我们提供id的配置,这个id标识了我们在localStorage对象中的数据。
现在Note这个store具备了利用localStorage读取和保存数据的功能。我们可以回到视图和控制器中保存数据的函数中。
Adding Event Listeners To A Sencha Touch Controller(给控制器添加事件监听器)
In the NoteEditor Class, let’s modify the saveButton declaration in the initialize() function like so:
我们修改NoteEditor类中初始化函数中saveButton的声明:
var saveButton = { xtype: "button", ui: "action", text: "Save", handler: this.onSaveButtonTap, scope: this };
The handler config defines the handler function that will run when a user taps the button. To make sure we run the handler in the scope of the NoteEditor, we set the scope config to this.Now we can define onSaveButtonTap() as follows:
这个handler配置项定义了按钮的点击处理函数。通过将socope配置项配置为this,来确保这个handler的scope(生命周期)在NoteEditor类中。现在我们可以向下面一样定义onSaveButtonTap()函数。
onSaveButtonTap: function () { console.log("saveNoteCommand"); this.fireEvent("saveNoteCommand", this); }
No surprises here, right? As we’ve done with other handlers, we are capturing the Button tap event within the View and defining a View event, saveNoteCommand.
这也没什么奇怪,对不对?这和我们所做的与其他按钮事件的处理一样,并不在视图中处理事件,而是在视图中捕捉到此事件后将其广播,然后再控制器中捕捉和进行处理。
As we already know, the fact that the NoteEditor View broadcasts the saveNoteCommand event is not enough for the Controller to be able to listen to it. We need to tell the Controller where this event is coming from, which we did when we added the noteEditor entry in the refs config of the Controller:
我们已经知道,事实上我们只广播这个事件是不够的,我们还应该在控制器中通过refs配置事件源和事件处理函数。
refs: { // We're going to lookup our views by xtype. notesListContainer: "noteslistcontainer", noteEditor: "noteeditor" }
We will use the Controller’s control config to map the saveNoteCommand event to a handler function. Therefore, need an entry for the Note Editor in the control config:
我们使用控制器的control配置项来映射事件处理函数:
control: { notesListContainer: { // The commands fired by the notes list container. newNoteCommand: "onNewNoteCommand", editNoteCommand: "onEditNoteCommand" }, noteEditor: { // The commands fired by the note editor. saveNoteCommand: "onSaveNoteCommand" } }
Finally, we will define the onSaveNoteCommand() function like so:
最后我们这样定义onSavenNoteCommand()函数:
onSaveNoteCommand: function () { console.log("onSaveNoteCommand"); var noteEditor = this.getNoteEditor(); var currentNote = noteEditor.getRecord(); var newValues = noteEditor.getValues(); // Update the current note's fields with form values. currentNote.set("title", newValues.title); currentNote.set("narrative", newValues.narrative); var errors = currentNote.validate(); if (!errors.isValid()) { Ext.Msg.alert('Wait!', errors.getByField("title")[0].getMessage(), Ext.emptyFn); currentNote.reject(); return; } var notesStore = Ext.getStore("Notes"); if (null == notesStore.findRecord('id', currentNote.data.id)) { notesStore.add(currentNote); } notesStore.sync(); notesStore.sort([{ property: 'dateCreated', direction: 'DESC'}]); this.activateNotesList(); }
We begin onSaveNoteCommand() acquiring(采集) references to the note being edited and the values in the form’s fields:
在onSaveNoteCommand()函数的开始我们从表单中采集数据:
var noteEditor = this.getNoteEditor(); var currentNote = noteEditor.getRecord(); var newValues = noteEditor.getValues();
Then, we transfer the new values to the loaded note:
然后我们把这个新值传递给加载的note:
currentNote.set("title", newValues.title); currentNote.set("narrative", newValues.narrative);
Model Validation In Sencha Touch(在ST数据模型验证)
Next comes an important part, which is validation. To validate the new values we loaded into the model instance, we first call the model’s validate() function, and then call the isValid() function on the errors object returned by validate():
接下来是很重要的一个部分,就是验证。为了验证我们加载到数据模型实例中的数据,我们首先调用了数据模型的validate()函数,然后再调用validate()函数返回的错误对象的isValid()函数:
var errors = currentNote.validate(); if (!errors.isValid()) { Ext.Msg.alert('Wait!', errors.getByField("title")[0].getMessage(), Ext.emptyFn); currentNote.reject(); return; }
The Ext.data.Model’s validate() function iterates over(迭代) the validations defined for the model, and returns an Ext.data.Errors instance containing(包含) Ext.data.Error instances for each model field that is invalid.
Ext.data.Model类的validate()函数迭代了在model中定义的验证并且返回一个Ext.data.Error实例。
In our case, the only field with an attached(附加) validator is the note’s title. When we find that the model is invalid, we first display an alert using the validator’s configured(设定的,配置的) message, and then call the model’s reject() function. This function reverts the modified fields back to their original values before we exit the onSaveNoteCommand() handler.
在本例中,唯一附加验证的是note的title字段。当发现model无效时,会弹出一个消息框,这个消息在model的validate中就已经定义了。然后调用model的reject()函数,这个函数在我们对出onSaveCommand()函数之前将修改过的字段恢复到他们的原先值。
Saving Data Using LocalStorageProxy(使用本地仓库代理保存数据)
In onSaveNoteCommand(), after confirming that the modified note is valid, we move on to(转到,进入到) save it on the device:
在onSaveNoteCommand()函数中,验证修改过的note有效之后,我们开始在设备上保存数据:
var notesStore = Ext.getStore("Notes"); if (null == notesStore.findRecord('id', currentNote.data.id)) { notesStore.add(currentNote); } notesStore.sync();
As this routine(惯例,通常) works for new or edited notes, we need to find out if the note is new by searching the store using its findRecrod() function. If the note is new, we add it to the store.
通常来说,我们应该在保存note之前使用findRecord()函数查看store中是否含有同样的note。如果没有,我们就把这个新note添加到sotre中。
The store’s sync() function asks the store’s proxy to process all the changes, effectively saving new or edited notes, and removing deleting notes from localStorage.After the store’s records have been updated, we sort them by date:
Sotre的sync()函数访问store的代理来同步所有的更新。
Store中的数据更新之后,我们把他们根据日期排序:
notesStore.sort([{ property: 'dateCreated', direction: 'DESC'}]);
Returning To The Main View(返回到主视图)
Our last step in onSaveNoteCommand() consists of invoking the activateNotesList() function. This is a helper function, similar to activateNoteEditor(), that will make the app’s main View active:
最后一步在onSaveNoteCommand()函数中包括调用activeNotesList()函数。这个函数很有用,就像activeNoteEditor()函数一样,他会激活应用的主视图。
activateNotesList: function () { Ext.Viewport.animateActiveItem(this.getNotesListContainer(), this.slideRightTransition); }
In this case we are using a right-slide transition, which we will define like so:
在这个案例中我们使用了右滑动过度,它是这样定义的:
slideRightTransition: { type: 'slide', direction: 'right' }
OK. What do you think about doing another quick check? This time we should be able to save a note:
好了,现在你可以保存note了。
Summary
In this article we added a couple of features to the Notes App: the ability to create notes, and edit existing notes.
在这篇文章中我们添加了一些特性:创建和编辑存在的笔记。
We started by learning how to use Sencha Touch’s Form components to edit data in an application, and how to use a LocalStorageProxy instance to cache data on the device running the application. We also learned how to validate a Sencha Touch data Model, synchronize(同步) a Data Store with its Proxy, and revert(还原) Model changes when its data is invalid(无效).
我们从在ST中使用表单控件编辑数据和使用LocalsLocalStorageProxy实例在设备上缓存数据开始学习。我们也学习了如何验证ST的数据模型,使用Store的代理同步数据和当数据无效时还原数据模型。
In addition(此外), we learned how to create transitions(转换) and connect different Views in a multi-view application.
此外我们学习了如何在多视图的应用中创建转换和联系。
At this point the application is missing the ability to delete notes. We will add this feature in the next part of this tutorial, where we will also modify the Notes List so it renders the cached notes grouped by date.
到此为止,应用还缺少删除功能,我们会在教程的下一节添加此特性。在下一节中我们还会修改日记列表使他能够按照日期分组。
Stay tuned!
敬请期待!
Downloads
Download the source code for this article: NotesApp-ST2-Part3.zip
The Entire Series
§ How to Create a Sencha Touch 2 App, Part 1
§ How to Create a Sencha Touch 2 App, Part 2
§ How to Create a Sencha Touch 2 App, Part 3
§ How to Create a Sencha Touch 2 App, Part 4
§ How to Create a Sencha Touch 2 App, Part 5
§
Want To Learn More?
My Sencha Touch books will teach you how to create an application from scratch.
§ Sencha Touch 2 Book
§ Sencha Touch 1 Book
TAGGED WITH: SENCHA TOUCH TUTORIAL 67 COMMENTS
About Jorge
Jorge is the author of Building a Sencha Touch Application, How to Build a jQuery Mobile Application, and the Ext JS 3.0 Cookbook. He runs a software development and developer education shop that focuses on mobile, web and desktop technologies.
If you'd like to work with Jorge, contact him at ramonj[AT]miamicoder.com.