Sencha Tutorial Part 2


we will continue building a small application that allows people to save notes on the device running the app.

So far, we have been working on the View that renders the list of notes cached on the device:

 

While building this View, we defined the NotesListContainer Class. We are going to start this article with a modification to this Class. This modification will promote encapsulation, and make the app easier to change and maintain.

We had previously defined the NotesListContainer Class as follows:

1 Ext.define("NotesApp.view.NotesListContainer", {
2     extend: "Ext.Container",
3     config: {
4         items: [{
5             xtype: "toolbar",
6             docked: "top",
7             title: "My Notes",
8             items: [{
9                 xtype: "spacer"
10             }, {
11                 xtype: "button",
12                 text: "New",
13                 ui: "action",
14                 id:"new-note-btn"
15             }]
16         }]
17     }
18 });

The changes we will make to this View consist of using the Class’s initialize() function to define its components. We will begin creating a new Class definition like so:

1 Ext.define("NotesApp.view.NotesListContainer", {
2     extend: "Ext.Container",
3     alias: "widget.noteslistcontainer",
4  
5     initialize: function () {
6  
7         this.callParent(arguments);
8  
9     }
10 });

Notice how we are using the alias config. This config is very helpful, as it effectively defines an xtype for our Class. Thanks to the alias, we can now refer to the NotesListContainer Class with the xtype=”noteslistcontainer” config. We will use this construct to instantiate the Class later in the article,

In Sencha Touch 2, every Class has an initialize() function. Initialize() can be used to perform logic right after the Class is instantiated, it replaces the initComponent() function that exists in Sencha Touch 1, and we can use it to add the toolbar with the New button:

1 Ext.define("NotesApp.view.NotesListContainer", {
2     extend: "Ext.Container",
3     alias: "widget.noteslistcontainer",
4  
5     initialize: function () {
6  
7         this.callParent(arguments);
8  
9         var newButton = {
10             xtype: "button",
11             text: 'New',
12             ui: 'action',
13             handler: this.onNewButtonTap,
14             scope: this
15         };
16  
17         var topToolbar = {
18             xtype: "toolbar",
19             title: 'My Notes',
20             docked: "top",
21             items: [
22                 { xtype: 'spacer' },
23                 newButton
24             ]
25         };
26  
27         this.add([topToolbar]);
28     },
29     onNewButtonTap: function () {
30         console.log("newNoteCommand");
31         this.fireEvent("newNoteCommand"this);
32     },
33     config: {
34         layout: {
35             type: 'fit'
36         }
37     }
38 });

In the initialize() function, after invoking callParent(), we define the New button variable, along with the Toolbar. As you already know from the previous chapter, the Toolbar’s items are the spacer and the Button.

Our last step within initialize() consists of adding the Toolbar to the View’s items via a call to the add() function.

If you go back to the newButton definition, you will notice that we’ve added a tap handler using the handler config:

1 var newButton = {
2     xtype: "button",
3     text: 'New',
4     ui: 'action',
5     handler: this.onNewButtonTap,
6     scope: this
7 };

This function will capture tap events on the button, and transform them into an event that is more specific and descriptive of the application’s business logic. We will call this event newNoteCommand. Here’s the handler’s implementation:

1 onNewButtonTap: function () {
2     console.log("newNoteCommand");
3     this.fireEvent("newNoteCommand"this);
4 }

This is one of the important changes we are making to the NotesListContainer Class. In the first article of the series, the tap event on the button was captured by the Controller:

1 Ext.define("NotesApp.controller.Notes", {
2     extend: "Ext.app.Controller",
3     config: {
4         refs: {
5             newNoteBtn: "#new-note-btn"
6         },
7         control: {
8             newNoteBtn: {
9                 tap: "onNewNote"
10             }
11         }
12     },
13     onNewNote: function () {
14         console.log("onNewNote");
15     }
16  
17     // Rest of the controller's code omitted for brevity.
18 });

Now, we’re capturing the event within the View, and broadcasting a new event, newNoteCommand, which will be captured by the Controller:

1 onNewButtonTap: function () {
2     console.log("newNoteCommand");
3     this.fireEvent("newNoteCommand"this);
4 }

Although both approaches are valid, there are important benefits derived from the second approach:

  • The View’s interface is cleaner. It now fires events that are more in line with the business logic of the application.
  • The View is easier to modify and maintain, as the Controller does not need intimate knowledge of the View’s inner workings.

As long as the View’s public events remain the same, the Controller will be able to work with the View. For example, we can change the elements used to trigger the creation of a new note in the View without affecting the Controller. The Controller only needs to listen to the newNoteCommand event fired from the view.

The next step of this refactoring will take place in the app.js file, where we will modify the application() function so we create our NoteListContainer instance using the Class’s alias:

1 Ext.application({
2     name: "NotesApp",
3  
4     controllers: ["Notes"],
5     views: ["NotesListContainer"],
6  
7     launch: function () {
8  
9         var notesListContainer = {
10             xtype: "noteslistcontainer"
11         };
12  
13         Ext.Viewport.add(notesListContainer);
14     }
15 });

And finally, we will switch over to the controller and modify the refs section just so we lookup our ref by xtype instead of by id:

1 Ext.define("NotesApp.controller.Notes", {
2  
3     extend: "Ext.app.Controller",
4     config: {
5         refs: {
6             // We're going to lookup our views by xtype.
7             notesListContainer: "noteslistcontainer"
8         },
9         control: {
10             notesListContainer: {
11                 // The commands fired by the notes list container.
12                 newNoteCommand: "onNewNoteCommand",
13                 editNoteCommand: "onEditNoteCommand"
14             }
15         }
16     },
17  
18     // Commands.
19     onNewNoteCommand: function () {
20  
21         console.log("onNewNoteCommand");
22     },
23     onEditNoteCommand: function (list, record) {
24  
25         console.log("onEditNoteCommand");
26     },
27     // Base Class functions.
28     launch: function () {
29         this.callParent(arguments);
30         console.log("launch");
31     },
32     init: function () {
33         this.callParent(arguments);
34         console.log("init");
35     }
36 });

Notice that we also added the onEditNoteCommand event and editNoteCommand handler to the controller. We will define the onEditNoteCommand in the next section of the tutorial, when we create the Notes List View.

After these changes, we can open the index.html page in our favorite WebKit browser, to confirm that everything is working as expected. Tapping the New button should produce the console message we added to the onNewButtonTap function:

Creating The Notes List View

The Notes List View is the component that will render the cached notes. Its file is NotesList.js, which we will place in the view folder. To create this component, we will extend the Sencha Touch’s Ext.dataview.List Class:

1 Ext.define("NotesApp.view.NotesList", {
2     extend: "Ext.dataview.List",
3     alias: "widget.noteslist",
4     config: {
5         loadingText: "Loading Notes...",
6         emptyText: '</pre>
7 <div class="notes-list-empty-text">No notes found.</div>
8 <pre>',
9         onItemDisclosure: true,
10         itemTpl: '</pre>
11 <div class="list-item-title">{title}</div>
12 <div class="list-item-narrative">{narrative}</div>
13 <pre>',
14     }
15 });

In this definition, we are setting the onItemDisclosure config to true, which means that we want the list to display a disclosure button next to each item:

A tap on the disclosure button will trigger the Note editing feature of the application. We will create the disclosure handler function in a few minutes.

In the NotesList Class, pay attention to the different CSS classes we use in the itemTpl and emptyText configs. They will allow us to nicely format both the list items, and the message the list will show when there are no items to display.

Before creating these styles we need to create the app.css file. We will place the file in the resources/css directory:

Here are the styles we need:

1 /* Increase height of list item so title and narrative lines fit */
2 .x-list .x-list-item .x-list-item-label
3 {
4      min-height3.5em!important;
5 }
6 /* Move up the disclosure button to account for the list item height increase */
7 .x-list .x-list-disclosure {
8 positionabsolute;
9 bottom0.85em;
10 right0.44em;
11 }
12 .list-item-title
13 {
14     float:left;
15     width:100%;
16     font-size:90%;
17     white-spacenowrap;
18     overflowhidden;
19     text-overflow: ellipsis;
20     padding-right:25px;
21     line-height:150%;
22 }
23 .list-item-narrative
24 {
25     float:left;
26     width:95%;
27     color:#666666;
28     font-size:80%;
29     white-spacenowrap;
30     overflowhidden;
31     text-overflow: ellipsis;
32     padding-right:25px;
33 }
34 .x-item-selected .list-item-title
35 {
36     color:#ffffff;
37 }
38 .x-item-selected .list-item-narrative
39 {
40     color:#ffffff;
41 }
42 .notes-list-empty-text
43 {
44     padding:10px;
45 }

In order to render the NotesList instance, we first need to add the Class name to the views config of the application:

1 views: ["NotesList""NotesListContainer"]

Then, we need to add it to the NotesListContainer Class. Back in the NotesListContainer.js file, we will add the notesList variable to the initialize() function:

1 Ext.define("NotesApp.view.NotesListContainer", {
2     extend: "Ext.Container",
3     alias: "widget.noteslistcontainer",
4  
5     initialize: function () {
6  
7         this.callParent(arguments);
8  
9         var newButton = {
10             xtype: "button",
11             text: 'New',
12             ui: 'action',
13             handler: this.onNewButtonTap,
14             scope: this
15         };
16  
17         var topToolbar = {
18             xtype: "toolbar",
19             title: 'My Notes',
20             docked: "top",
21             items: [
22                 { xtype: 'spacer' },
23                 newButton
24             ]
25         };
26  
27         var notesList = {
28             xtype: "noteslist",
29             listeners: {
30                 disclose: { fn: this.onNotesListDisclose, scope: this }
31             }
32         };
33  
34         this.add([topToolbar, notesList]);
35     },
36     onNewButtonTap: function () {
37         console.log("newNoteCommand");
38         this.fireEvent("newNoteCommand"this);
39     },
40     config: {
41         layout: {
42             type: 'fit'
43         }
44     }
45 });

Notice how we are setting a listener for the disclose event of the list:

1 var notesList = {
2     xtype: "noteslist",
3     listeners: {
4         disclose: { fn: this.onNotesListDisclose, scope:this }
5     }
6 };

Now we can define the onNotesListDisclose() function as follows:

1 onNotesListDisclose: function (list, record, target, index, evt, options) {
2     console.log("editNoteCommand");
3     this.fireEvent('editNoteCommand'this, record);
4 }

Here we are taking the approach we followed earlier with the New button. Instead of having the Controller listen to the disclose event of the List, we are hiding this event, and creating the editNoteCommand event, which we will expose to the Controller. This is another step towards making the application more flexible and easier to maintain.

Creating a Sencha Touch Data Model To Represent a Note

The Notes List requires a data store, which will supply the information for its list items. In order to create this store, we first need to define a data model that will represent a note.

Let’s go ahead and define the Note Class. We will place this Class in the Note.js file, which we will save in the model directory:

A note will have four fields: id, date created, title and narrative. We will start with the following definition:

1 Ext.define("NotesApp.model.Note", {
2     extend: "Ext.data.Model",
3     config: {
4         idProperty: 'id',
5         fields: [
6             { name: 'id', type: 'int' },
7             { name: 'dateCreated', type: 'date', dateFormat: 'c' },
8             { name: 'title', type: 'string' },
9             { name: 'narrative', type: 'string' }
10         ]
11     }
12 });

We will use the idProperty config to establish that the id field is actually the field the framework can use to uniquely identify a note. This seems trivial in our case because we have total control over the names of the fields of the data model. However, you might encounter cases where, for example, the data model’s fields are tightly coupled to column names in an existing database, and the name of the column that uniquely identifies a record is not “id”. This is why the idProperty config is important.

Setting Up Model Validation In Sencha Touch

The id, dateCreated and title fields in our Note Model are mandatory. We will express this requirement using the validations config:

1 Ext.define("NotesApp.model.Note", {
2     extend: "Ext.data.Model",
3     config: {
4         idProperty: 'id',
5         fields: [
6             { name: 'id', type: 'int' },
7             { name: 'dateCreated', type: 'date', dateFormat: 'c' },
8             { name: 'title', type: 'string' },
9             { name: 'narrative', type: 'string' }
10         ],
11         validations: [
12             { type: 'presence', field: 'id' },
13             { type: 'presence', field: 'dateCreated' },
14             { type: 'presence', field: 'title', message:'Please enter a title for this note.' }
15         ]
16     }
17 });

For the title field, we are taking advantage of the message config to define the message the user will see when she tries to save a note without typing in its title.

Before moving on to create the data store, we need to add the model to the models config of the application:

1 Ext.application({
2     name: "NotesApp",
3  
4     models: ["Note"],
5  
6     // Rest of the app's definition omitted for brevity...
7 });

Creating a Sencha Touch Data Store

This is all we need for our data model at this point. Now we can focus on creating the data store that will feed the List. The Notes store goes in a new file. We will place this file in the store directory:

For now, the store will simply contain a few hard-coded records:

1 Ext.define("NotesApp.store.Notes", {
2     extend: "Ext.data.Store",
3     config: {
4         model: "NotesApp.model.Note",
5         data: [
6             { title: "Note 1", narrative: "narrative 1" },
7             { title: "Note 2", narrative: "narrative 2" },
8             { title: "Note 3", narrative: "narrative 3" },
9             { title: "Note 4", narrative: "narrative 4" },
10             { title: "Note 5", narrative: "narrative 5" },
11             { title: "Note 6", narrative: "narrative 6" }
12         ]
13     }
14 });

Worth highlighting here is the model config, which we use to establish that this store will contain instances of the Note model.

We want the Notes List to render the notes sorted by creation date. This is why we will add the sorters config to the store’s definition:

1 Ext.define("NotesApp.store.Notes", {
2     extend: "Ext.data.Store",
3     requires: "Ext.data.proxy.LocalStorage",
4     config: {
5         model: "NotesApp.model.Note",
6         data: [
7             { title: "Note 1", narrative: "narrative 1" },
8             { title: "Note 2", narrative: "narrative 2" },
9             { title: "Note 3", narrative: "narrative 3" },
10             { title: "Note 4", narrative: "narrative 4" },
11             { title: "Note 5", narrative: "narrative 5" },
12             { title: "Note 6", narrative: "narrative 6" }
13         ],
14         sorters: [{ property: 'dateCreated', direction:'DESC'}]
15     }
16 });

Now we can jump back to the NotesListContainer.js file, and add the store to the notesList declaration in the NotesListContainer Class:

1 var notesList = {
2     xtype: "noteslist",
3     store: Ext.getStore("Notes"),
4     listeners: {
5         disclose: { fn: this.onNotesListDisclose, scope:this }
6     }
7 };

Before we check how we are doing, let’s quickly switch over to the Controller’s definition in the controller/Notes.js file, locate the launch() function, and invoke the store’s load() function as follows:

1 launch: function () {
2     this.callParent(arguments);
3     Ext.getStore("Notes").load();
4     console.log("launch");
5 }

We really don’t need this call now – remember that the data is hard-coded – but we are adding in preparation for the next chapter of the tutorial, where we will discontinue the use of hard-coded data, and begin using data stored in the browser’s cache.

What we need to do, though, is add a reference to the store in the app.js file:

1 Ext.application({
2     name: "NotesApp",
3  
4     models: ["Note"],
5     stores: ["Notes"],
6     controllers: ["Notes"],
7     views: ["NotesList""NotesListContainer"],
8  
9     launch: function () {
10  
11         var notesListContainer = {
12             xtype: "noteslistcontainer"
13         };      
14  
15         Ext.Viewport.add(notesListContainer);
16     }
17 });

Excellent! At this point we should be able to see the hard-coded notes rendered on the screen. Let’s start our emulator, where we should see something like this:

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Create a free website or blog at WordPress.com.

Up ↑

%d bloggers like this: