Continued from Creating Mobile Apps with Sencha Touch 2 – Part 1
Since the editing and creating note screen going to be same, so we can share the same screen.
app/view/NoteEditor.js
Ext.define("SenchaNote.view.NoteEditor", {
extend: "Ext.form.Panel",
xtype: "noteeditor",
requires: ["Ext.form.FieldSet", "Ext.field.Select"],
initialize: function() {
var toolbar = {
xtype: "toolbar",
docked: "top",
title: "Add Note",
items: [
{
xtype: "button",
ui: "back",
text: "Back",
handler: this.onBackTap,
scope: this
},
{ xtype: "spacer" },
{
xtype: "button",
ui: "confirm",
text: "Save",
handler: this.onSaveTap,
scope: this
}
]
};
this.add([
toolbar,
{
xtype: "fieldset",
defaults: { //this set default value to every items added
xtype: "textfield"
},
items: [
{
name: "content",
label: "Content"
},
{
name: "categoryid",
label: "Category",
xtype: "selectfield",
store: "Category",
displayField: "name",
valueField: "id"
}
]
}
]);
},
config: {
listeners: {
show: function() { this.onShow(); }
}
},
onShow: function() {
if (this.getRecord().phantom) {
this.items.get(0).setTitle("Add Note");
} else {
this.items.get(0).setTitle("Edit Note");
}
},
onSaveTap: function() {
//fire event to controller
},
onBackTap: function() {
//fire event to controller
}
});
Here we need to use the Ext.form.Panel, and import the Ext.form.FieldSet and Ext.form.Select. Then the usual stuff, create a toolbar, docked it on the top, give it a title “Add Note”, and add a back button and a save button on it, and assign handler to both buttons.
After creating the object of toolbar, add it to the screen using this.add just like the search bar (do it in initialize because we need to add the handler to the buttons.
In the this.add, we add another item on the fly (I mean without creating using the var ), use the built in xtype fieldset, note that there is a new thing, defaults, it simply means what ever item we configure later on will using the setting in defaults, unless specified otherwise.
So here I set default type to xtype:textfield, so that all item will default to a textbox. In the items, I need the content textbox. Note the { name: “price”, label :”Price”, xtype: “numberfield” } I commented is to show that we can override the defaults by specifying the xtype, and the numberfield here will allow the mobile browser display numeric keyboard, which made data entry easier.
Remember on the Note model, there is field id, content, categoryid and category. The id is use for backend, so there is no need of textbox here, the categoryid and category will be display as selection box, where displaying category as a text, but will save the categoryid to the server.
In order to create the dynamic populated selection, and since we’re saving the id instead of text, so on the items, we add the { name: “categoryid” } instead of category. Then we override the type to a selectfield (I no need to use defaults here since only got few items, but just to demonstrate), and bind the store named Category (will create later) to the select field. Finally use the displayField and valueField to bind to the column categoryid and category. Just like the <Option> in HTML.
Another new thing in configs, the listeners, which is use to listen to event triggered (check the documentation events section). Here I just simply call a function when the form is show on screen, why I’m doing this is because I’m sharing the screen for edit and add note. So naturally I want the title to display “Edit Note” and “Add Note” when adding new one.
The this.getRecord().phantom in the *onShow function means that the record (there will be only single record loaded when we reach this screen, will see how in controller later on), and the this.getRecord().phantom is use to check if there is existing record loaded or new record. So we’ll access the toolbar of this screen and use the setTitle function to change the title in toolbar.
The this.items.get(0) is simply get the first items in the screen, which depends on the sequence it added via the this.add function in the initialize function. Since our first added is toolbar, so we can use get(0) to get the toolbar.
Now, the brain! We’ve got the view ready, most of the model, and some dummy data, now what we need is controller to tell how the view works.
app/controller/Note.js
Ext.define("SenchaNote.controller.Note", {
extend: "Ext.app.Controller",
config: {
refs: {
mainPanel : "mainpanel",
noteListContainer: "notelistcontainer",
noteEditor: "noteeditor",
noteList: "notelist",
searchBar: "searchbar",
searchField: "searchbar > toolbar > searchfield"
},
control: {
noteListContainer: {
addNoteCommand: "onAddNote"
},
noteList: {
editNoteCommand: "onEditNote"
},
noteEditor: {
backCommand: "onBackButton",
saveCommand: "onSaveButton"
},
searchBar: {
searchNoteCommand: "onSearchNote"
}
}
},
onAddNote: function() {
var newNote = Ext.create("SenchaNote.model.Note");
this.getNoteEditor().setRecord(newNote);
Ext.Viewport.animateActiveItem(this.getNoteEditor(), { type: "slide", direction: "left" });
},
onEditNote: function(record) {
this.getNoteEditor().setRecord(record);
Ext.Viewport.animateActiveItem(this.getNoteEditor(), { type: "slide", direction: "left" });
},
onSearchNote: function() {},
onBackButton: function() {},
onSaveButton: function() {}
});
Usual thing again, define a controller with naming convention, then extend the Ext.app.Controller, and define few functions onAddNote, onEditNote, onSearchNote, onBackButton and onSaveButton so that we can fire the event from views later on.
The magic thing refs in the configs really magical. What it did was to create getter and setter for us on the views automagically. Note that each items in the refs are correspond to the xtype we’ve set for our views. E.g. we create the property named mainPanel and the corresponding view is the xtype of mainpanel.
The special case searchField: “searchbar > toolbar > searchfield”, it’s like a CSS selector, which it get the item xtype searchbar, then its item toolbar and finally the xtype item searchfield inside the toolbar, all of this can trace back on the SearchBar.js view.
The control is where we expose the event created (the onAddNote, onEditNote, onSearchNote, etc) to the view. Note that how the property name defined in the refs is used. And then we create an event item property and the corresponding event function. E.g. When a view is firing event “addNoteCommand” to the view NoteListContainer, we need to do this:
noteListContainer: {
addNoteCommand: "onAddNote"
},
and the addNoteCommand event will fire the local function onAddNote.
Now that we’ve exposed events to respective views, and defined our setter getter properties, we need to do something in the onAddNote and onEditNote function.
onEditNote: function(record)
this.getNoteEditor().setRecord(record);
Ext.Viewport.animateActiveItem(this.getNoteEditor(), { type: "slide", direction: "left" });
There, the this.getNoteEditor() is generate FOC by the refs we defined just now. Note that when we do noteEditor: “noteeditor” just now give us the getter method of getNoteEditor(). So we call the setRecord function of the note editor and pass in the record we get from the parameter (we’ll see this when we fix up the onItemDisclosure , the blue arrow button on the list). That line passes the record we tapped on the list to the note editor view.
Now that we’ve pass in the record, we need to swap the NoteEditor view to the screen, by calling the singleton Ext.Viewport.animateActiveItem , which required 2 parameters, first is the view we wanted to show, which is the this.getNoteEditor() that return our NoteEditor view.
The second parameter is the type of animation and direction, since the disclosure icon is at the right side of the screen, it’s natural to have the screen slide in from right to left.
onAddNote: function()
var newNote = Ext.create("SenchaNote.model.Note");
this.getNoteEditor().setRecord(newNote);
Ext.Viewport.animateActiveItem(this.getNoteEditor(), { type: "slide", direction: "left" });
The editor screen need to be bind with a record, either existing or new. So for add note, we create a new note model, get the note editor view and pass in the newly created model. After that set the view NoteEditor on top, and because of the newNote object we’ve created, the phantom property is working in the view there, so that we can set the title accordingly.
Since we’re here, we can add in the handler for onBackButton as well.
onBackButton: function()
Ext.Viewport.animateActiveItem(this.getMainPanel(), { type: "slide", direction: "right" });
Now we need to add in code in the NoteList view’s onItemDisclosure stub method we created, to fire event to our controller.
app/view/NoteList.js – onItemDisclosure: function(record,btn,index)
this.fireEvent("editNoteCommand", record);
We fire event by the aptly name fireEvent , and pass in the event name exposed from our note controller, and the record we get from the parameter. See how it all link together nicely.
Now add to the NoteListContainer’s add button as well
app/view/NoteListContainer.js – onAddNoteTap: function()
this.fireEvent("addNoteCommand", this);
Now, if you remember, whenever we created new views/store/model, we need to inform the apps, this included the controller.
app.js
Ext.application({
name: "SenchaNote",
icon: "resources/images/app-icon.png",
phoneIcon: "resources/images/app-phone-icon.png",
controllers: ["Note"],
models: ["Note"],
stores: ["Note"],
views: ["Viewport", "MainPanel", "NoteListContainer", "NoteEditor"],
launch : function() {
Ext.create("SenchaNote.view.Viewport");
}
});
There, we add in our new controller, and new view. But it is useless to just inform the apps about the new view, we need to add it into our Viewport.
app/view/Viewport.js
Ext.define("SenchaNote.view.Viewport", {
extend: "Ext.Panel",
initialize: function() {},
config: {
fullscreen: true,
layout: "card",
items: [
{
xtype: "mainpanel"
},
{
xtype: "noteeditor"
}
]
}
});
There, using the xtype we defined.
One more thing, we still haven’t define our selection field for our category, it won’t run now as it looking for the store. So now we create a new model and store for it.
app/model/Category.js
Ext.define("SenchaNote.model.Category", {
extend: "Ext.data.Model",
config: {
idProperty: "id",
fields: [
{ name: "id", type: "integer" }, //need an id field else model.phantom won’t work correctly
{ name: "name", type: "string" }
],
validations: [
{ type: "presense", field: "id" },
{ type: "presense", field: "name" }
]
}
});
This is the similar model to the note model, very straight forward.
app/store/Category.js
Ext.define("SenchaNote.store.Category", {
extend: "Ext.data.Store",
requires: ["SenchaNote.model.Category"],
config: {
model: "SenchaNote.model.Category",
proxy: {
type: "ajax",
url: "http://192.168.168.10:8888/SenchaNote/categorylist.php",
reader: {
type: "json",
rootProperty: "categories"
}
},
data: [
{ id: 1, name: "Nonsense" },
{ id: 2, name: "Food" },
],
autoLoad: true
}
});
Resemblance to the Note store, but here we want it to auto load, it wouldn’t make sense to have user click button to load a selection field. We’ll add in some dummy data at the moment, note that our model field name is the same on we use for the displayField, valueField in NoteEditor view.
Now, remember, inform apps about the new model and store.
app.js
models: ["Note", "Category"],
stores: ["Note", "Category"],
Just need to add it the category in the existing one. So now when we tap on the list item on the arrow, editor screen will slide in and showing the form populated with the dummy data from note store, and the selection field is selected automagically, because the categoryid we bind is link with the id in the category store.
And when you tap on the field, the selection will popup. Note the label “Category” is too long to shown, it truncated, however every item there can be adjusted using CSS, check out the duckumention’s properties of textfield.
And when tap on the add button on main screen, we got empty field.
Now, we need to save the edited or new note.
app/controller/Note.js – onSaveButton:function()
var currentNote = this.getNoteEditor().getRecord();
var newValue = this.getNoteEditor().getValues();
currentNote.set("content", newValue.content);
currentNote.set("categoryid", newValue.categoryid);
var errors = currentNote.validate();
if (!errors.isValid()) {
currentNote.reject();
return;
}
var noteStore = Ext.getStore("Note");
if (null == noteStore.findRecord("id", currentNote.data.id)) {
noteStore.add(currentNote);
}
noteStore.sync();
The logic here is easy, and the one and only save button will do good for both editing and creating. It first get the record from the NoteEditor, and then get the values from the NoteEditor, and then set the new value into the existing record, note that the field we using is similar with the model.
Remember we have validation in model? Now we can use it, by calling validate(), and check using the isValid(), if it’s not valid, we’ll call the reject() to discard changes. Pretty straight forward.
After validation, we need to check with the store record retrieve from server, using the Ext.getStore singleton, and use the findRecord function, passing in the existing record id and search for id in the store. If it return nothing, means it’s a new note, so we call the add function in the store. If it found, we no need to do anything, because the final sync() wil sync back everything nicely, either new or edited, very straight forward.
Now we won’t be seing anything yet, because we use dummy data and did not have server side script yet.
For the search, add this in to the onSearchNote function in controller:
app/controler/Note.js – onSearchNote: function()var store = Ext.getStore(“Note”);
store.load({
params: {
action: “search”,
keyword: this.getSearchField().getValue()
},
scope: this
});
*explain in part 3
var store = Ext.getStore("Note");
store.getProxy().setExtraParam("keyword", this.getSearchField().getValue());
store.loadPage(1);
Another simple function, it simply retrieve the note store (of course! we searching for note), and then call the load function with the params. Params is the extra query string parameter we can pass along in the url. Here I have 2 query string, action and keyword, action I just simply pass in “Search”, and keyword I use the property getSearchField() generated automagically with refs , and use the getValue() to get the textbox value.
app/view/SearchBar.js – onSearch:function()
this.fireEvent("searchNoteCommand", this);
The load function will trigger the proxy in the Note store API read that we commented out. I’ll leave this for part 3, as what we left now is CRUD operation back to server, displaying search results and paging.
Related posst: