Data Stores - Building the FindACab App - Hands-On Sencha Touch 2 (2014)

Hands-On Sencha Touch 2 (2014)

Part II. Building the FindACab App

Chapter 9. Data Stores

A data store is a mechanism to cache your data and is part of the Ext.data package. It is like a bucket full of data records. You can pick (select) a record out of this bucket (the data store) and add or remove records. Stores also provide functions for sorting, filtering, and grouping the model instances. You’ll need to give a model structure to the store with data. You can do this inline by setting the fields and data arrays (hardcoded), but a better MVC approach is to bind a model to the data store.

Sencha Touch has data-aware components—such as lists, dataviews, and charts—that need to be hooked up to a store in order to display data. I will discuss those in Chapter 11.

In this chapter, you’ll learn:

§ How to load data in a store

§ How to sort a data store locally

§ How to sort data on a server

§ How to group a data store

§ How to filter a data store locally

§ How to filter a data store on a server

§ How to save/sync data in a store

Loading Data in a Store

For the FindACab app to be able to display the data, the data needs to be contained in the store. By default, when you create a store (and the data is not hardcoded), you will have to load the model data into your store.

When autoLoad is not enabled, you have to manually load the store from your code, or from your developer’s console:

Ext.getStore('MyStore').load(this, records, successful, operation, eOpts);

NOTE

Ext.getStore("MyStore") is a lookup method; it finds a store (if the store is registered in the Ext.application() or controller) based on the store instance name or storeId through the StoreManager. Really, it’s a short alias for Ext.data.StoreManager.lookup("myStore");.

We want the FindACab app to retrieve a list of cabs in the area. We already hooked up a proxy to the store, so we can load the data. When you run the Ext.getStore("Cabs").load() event in the console, it will look up the Cabs store through the StoreManager and return a store object with a data array that contains 20 items.

Instead of just loading the store, you can also handle a callback:

Ext.getStore('Cabs').load({

callback: function(records, success, operation) {

//callback function here

console.log(records);

},

scope: this

});

The store has a callback function, which in this case logs all records after the store is loaded. You can also set a scope. In this case, when you log console.log(this) in your callback, it won’t log the scope within the callback, but rather the scope of the class where the store load()event is called.

TIP

There are more events you can listen for in the store; for example, addrecords, beforeload, beforesync, refresh, removerecords, updaterecords, and write. Check the API docs for more details about the different store events.

Let’s go back to the FindACab app and modify the Utils.Commons class (which we created in Chapter 4) so the Yelp API and API key are saved in a central place:

statics: {

YELP_API: 'http://api.yelp.com/business_review_search?',

YELP_KEY: 'yelp-key-here',

YELP_TERM: 'Taxi'

}

Now you will modify the store proxy config. Instead of entering a full proxy URL, you will retrieve the URL, the YELP_API key, and the YELP_TERM from the Utils.Commons static file, so it’s better organized. You can send parameters with the request by using the extraParams object, and you can modify these parameters from elsewhere in your code, as shown in Example 9-1.

Example 9-1. store/Cabs.js

Ext.define('FindACab.store.Cabs', {

extend: 'Ext.data.Store',

requires: ['Ext.data.proxy.JsonP'],

config: {

model: 'FindACab.model.Cab',

autoLoad: false,

proxy: {

type: 'jsonp',

url: Utils.Commons.YELP_API,

noCache: false,

extraParams: {

term: Utils.Commons.YELP_TERM,

ywsid: Utils.Commons.YELP_KEY,

location: Utils.Commons.LOCATION

},

reader: {

type: 'json',

rootProperty: 'businesses',

}

},

}

});

In order to maintain the store callback in the controller, you will create a system event listener to listen to the store load event. For now, this code will only log the results, and show and hide a loading indicator. Example 9-2 shows the new FindACab.controller.CabController.

Example 9-2. controller/CabController.js

Ext.define('FindACab.controller.CabController', {

extend: 'Ext.app.Controller',

config: {

models: ['Cab'],

stores: ['Cabs']

},

init: function() {

Ext.Viewport.mask({

xtype: 'loadmask',

message: 'loading...'

});

Ext.getStore('Cabs').load();

Ext.getStore('Cabs').addListener('load',

this.onCabsStoreLoad,

this);

},

onCabsStoreLoad: function(records, success, operation) {

console.log(records.getData());

Ext.Viewport.unmask();

}

});

After initializing the controller, this code will load the Cabs store, and add a load listener to listen to the load system event of the store. It will also add a loading animation to the application viewport, Ext.Viewport.mask(), that spins a loading animation. When a load() event callback comes in, it will run the function onCabsStoreLoad(). This will print the received data object into your debugging console and hide the loading application by setting Ext.Viewport.unmask().

So far, so good: you have all the data in your app. There are nice ways to manipulate your store results collections. For example, you can sort, filter, or group a store, as we’ll discuss in the next section.

Sorting a Data Store Locally

After you retrieve data in your store, you might notice that the store is not sorted. It is possible to sort the records in a data store on the client side. You will use the Ext.data.Store.sort(sorters, [defaultDirection], [where]) method, and you can pass in sorters_ object, which specifies the fieldname to sort and the direction, either ASC (ascending, A–Z) and DESC (descending, Z-A).

Here I construct a sorters array to sort the fieldname property by ASC:

sorters: [{

property: "fieldname",

direction: "ASC"

}]

The sorters array or the sort() method on the store sorts the data collection inside the store by one or more of its properties.

To programmatically sort a store from elsewhere in your code, you can pass in a single argument, the fieldname to sort. This will toggle between ascending and descending:

Ext.getStore("Cabs").sort("name");

Or you can pass in the full sorters configuration:

Ext.getStore("Cabs").sort({

property: "fieldname",

direction: "ASC"

});

Or just the string fieldname and strings "ASC" or "DESC":

Ext.getStore("Cabs").sort("name", "DESC");

In the FindACab app, you will sort the Cabs list on the cab service name in alphabetical order. Therefore, the default sorter will be set to name:

sorters: [{

property: "name",

direction: "ASC"

}],

It is possible to add sorters on top of each other. For example, first sort on the field name and afterward filter on the field distance. You do so by passing an array:

store.sort([

{

property : 'name',

direction: 'ASC'

},

{

property : 'distance',

direction: 'DESC'

}

]);

When the sort() method is called again with a new sorter object, any existing sorters will be removed. When the sort() method is called without any arguments, the existing sorters will be reapplied. To keep existing sorters and add new ones, you will need to run the add() method on thesorters object. Here is how you can add sorters to a store:

store.sorters.add(new Ext.util.Sorter({

property : 'phone',

direction: 'ASC'

}));

store.sort();

The previous examples make sense when you want to sort on local stores. However, it’s also possible to sort remotely on the server side. Let’s take a look at that in the next examples.

Sorting Data on a Server

The data that you retrieve from the server side might be very large. It could be faster to sort it on the server side instead of locally. Luckily, Sencha Touch provides a way to implement server-side paging, called remote sorting. You will use the Ext.data.Store.remoteSort boolean, and you will use the sorters object directly in the store or run the Ext.data.Store.sort(sorters, [defaultDirection], [where]) method where you can pass in a sorters object. (See the previous section on how to sort a store.)

If you want to enable remote sorting, set the following settings in the store class definition:

§ A pageSize to define the number of records that constitutes a “page.” (Note that the default page size is set to 25.)

§ The boolean remoteSort config in the store class definition to true. (Note that remote sorting is false by default.)

§ The sorters object, as described in the previous section:

pageSize: 30,

remoteSort: true,

sorters: [{

property: "fieldname",

direction: "ASC"

}]

Unfortunately, because we do not have control over the Yelp server side, we won’t implement a remote sorter for the FindACab app. However, I do want to share a running example of a remote sorter. In this demo, there is another data store with Car objects that sorts cars by brand in ascending order:

Ext.define('RemoteTest.store.Cars', {

extend: 'Ext.data.Store',

requires: ['Ext.data.proxy.JsonP'],

config: {

model: 'RemoteTest.model.Car',

autoLoad: true,

remoteSort: true, //1

sorters: [{ //2

property: "brand",

direction: "ASC"

}],

pageSize: 20, //3

proxy: { //4

type: 'jsonp',

url: 'http://someurl.com/test.php',

reader: { //5

rootProperty: 'results',

totalProperty: 'total',

successProperty: 'success'

}

},

}

});

1

Enable remote sorting.

2

Sort the cars by brand, in ascending order.

3

By setting the pageSize to 20, you are requesting 20 records per page from the server.

4

A JSONP proxy, to retrieve Car objects from http://<someurl>.com/test.php.

5

The proxy reader, which can read result, total, and success properties from the server response.

WARNING

The previous store has an autoLoad property. This makes sense for demo purposes, but in a real application, you would probably want to programmatically sort and load the store.

Currently, this Car store doesn’t do much. That’s because there is no server side implemented. This is OK, because the server-side code can be a black box for us. However, let’s assume that http://<someurl>.com/test.php is a working web service that sends Car objects back.

In the Google Developer network tab, you can see a request is made, which sends the following GET request to your server:

http://someurl.com/test.php?_dc=1386924081041&page=1&start=0&limit=20

&sort=%5B%7B%22property%22%3A%22brand%22%2C%22direction%22%3A%22ASC%22%7D%5D

&callback=Ext.data.JsonP.callback1

Let’s format the query string parameters:

page:1

start:0

limit:20

sort:[{"property":"name","direction":"ASC"}]

The limit parameter comes from the store pageSize. The page and start parameters are used for paging. On the server side, you can calculate which set of items you have to send back to the client-side code.

The http://<someurl>.com/test.php web service requires some logic to sort the data (e.g., in a database) and send the correct set of data back.

The server response for sending back Car objects (in PHP) could look like Example 9-3. The names of the success, total, and results properties should be set in the store’s reader.

Example 9-3. A server response in PHP

{

"success": true,

"total": 500,

"results": [{ "id": 1, "brand": "BMW", "type" : 7 },

{ "id": 2, "brand": "Mercedes", "type" : 5 }

... //20 results in total

]

}

Now that you know how to sort data, let’s discuss how to group it.

Grouping a Data Store

Grouping a data store makes sense when you want to display data into an Ext.List component in Sencha Touch and you want to visually group data. For example, when you have a store with companies, you could, for example, group by “city.” This will list every company per city.

To enable grouping in a store, implement the groupField and groupDir configurations directly in the store class definition. The groupFields sets the field to group and the groupDir sets the direction (ASC or DESC):

groupField: '<model-field-name>',

groupDir: 'ASC' //or DESC

To dynamically group a store, you can run the setGrouper() method on a store object:

Ext.getStore('Cabs').setGrouper({

direction: 'ASC', //or DESC

property: '<model-field-name>'

});

You will implement grouping on the Cabs store for the FindACab app list. This time, you will not group on city, because all the data that is in the Cabs store already shares the same city—for example, Amsterdam. Therefore, let’s group on the first alphabetical character of a cab service name. (See Figure 9-1.) You would see a group “A” that lists all names that start with an A, a group “B” that lists all names that start with a B, and so on. It’s the same behavior as when you open the contacts list on an iPhone. Names are grouped, and if you want, you can even display an index bar on the side to quickly browse to the corresponding character.

The Cabs store needs grouping to display taxi services in a grouped list

Figure 9-1. The Cabs store needs grouping to display taxi services in a grouped list

To achieve this, you will need the Ext.data.Store.grouper object, with a custom group function: groupFn(). You can set the grouper object directly in the store class definition, as shown in Example 9-4.

Example 9-4. app/store/Cabs.js

grouper: {

groupFn: function(record) {

return record.get('name')[0].toUpperCase();

}

}

The groupFn function with the code return record.get("name")[0].toUpperCase(); will group the data in the store on the first (uppercase) character of the name field.

Filtering a Data Store Locally

A data store can also filter records. When a filter is applied, the data store will not remove records. The same records are still available in the store, but only the records that match the filter criteria are displayed.

Filters are added as arrays. Here’s how to implement a filter array directly in the store class definition:

filters: [{

property: "fieldname",

value: "match"

}],

You can also filter programmatically. Just run the following method from a store instance:

Ext.data.Store.filter(filters, [value], [anyMatch], [caseSensitive]);

The filters array (or the filter() method on the store) filters the data collection by one or more of its filter properties, and returns only the data that matches the value of the filter.

It’s possible to filter on the first characters of a field or from anywhere (argument: anyMatch), and it’s also possible to filter for case sensitivity (argument: caseSensitive).

Custom Filter Functions

You can also create custom filter functions. To do so, you can set a filterFn in the array or use the filterBy(fn) method on the store.

Let’s implement a custom filter for the FindACab app. By default, the FindACab app is filtered by a function that checks whether a phone number is specified. This filters the Cabs store on phone numbers that have at least one character (see Example 9-5).

Example 9-5. app/store/Cabs.js

filters: [{

filterFn: function(item) {

return item.get("phone").length > 0;

}

}],

Stacking Filters

To add filters on top of each other—for example, to filter on a name with a value of Taxi and filter on a distance of 20 miles—you pass in an array. Here, I stack a couple of filters on top of each other:

store.filter([

{property: "name", value: "Taxi"},

{property: "distance", value: "20"}

]);

NOTE

Instead of passing an array with filter objects into the filter() method, I could call the filter() method again without the filter objects as arguments. Unlike sorters, filters won’t reset if you call them again. When you want to renew the filter, you have to clear it first:

store.clearFilter();

Filtering Data on a Server

The data that you retrieve from the server side might be very large. It could be faster to filter it on the server side instead of locally. Luckily, Sencha Touch provides a way to implement server-side filtering.

You will use the remoteFilter boolean and the array with filters. Here’s an example of the store class definition:

remoteFilter: true,

filters: [{

property: "fieldname",

value: "match"

}],

Here I set the boolean remoteFilter in the store to true to enable remote filtering (note, it is off by default).

Again, because we do not have server-side control over Yelp.com, we won’t implement a remote filter for the FindACab app. I do want to share an example of a remote filter, however. In this demo, there is another data store with Car objects and with remote filtering enabled. It has a filter set that filters on car brand:

Ext.define('RemoteTest.store.Cars', {

extend: 'Ext.data.Store',

requires: ['Ext.data.proxy.JsonP'],

config: {

model: 'RemoteTest.model.Car',

autoLoad: true,

pageSize: 20,

remoteFilter : true,

filters: [{

property: "brand",

value: "BMW"

}],

proxy: {

type: 'jsonp',

url: 'http://someurl.com/test.php',

reader: {

rootProperty: 'results',

totalProperty: 'total',

successProperty: 'success'

}

},

}

});

WARNING

Again, this store has an autoLoad property. This makes sense for demo purposes, but in a real application, you would probably want to programmatically filter and load the store.

When the remoteFilter configuration has been set to true, you will have to manually call the load method after every filter you set to retrieve the filtered data from the server.

Let’s assume that http://<someurl>.com/test.php is a working web service that sends Car objects back. We’ll filter on a car brand of BMW, 20 per time (page).

In the Google Developer Network tab, you can see a request is made that sends the following GET request to your server:

http://someurl.com/test.php?_dc=1387182737587&page=1&start=0&limit=20

&filter=%5B%7B%22property%22%3A%22brand%22%2C%22value%22%3A%22BMW%22%7D%5D

&callback=Ext.data.JsonP.callback1

Let’s format the query string parameters:

page:1

start:0

limit:20

filter:[{"property":"brand","value":"BMW"}]

As you might have noticed, the implementation and server requests of a remote filter are similar to the implementation and server requests of a remote sorter. The limit parameter comes from the store pageSize. The page and start parameters are used for paging. On the server side, you can calculate which set of items you have to send back to the client-side code.

The http://<someurl>.com/test.php requires some logic to filter their data (e.g., in a database), and send the correct set of data back.

The server response for sending back Car objects (in PHP) could look like Example 9-6. The names of the success, total, and results properties should be set in the store’s reader.

Example 9-6. A server response in PHP

{

"success": true,

"total": 500,

"results": [{ "id": 1, "brand": "BMW", "type" : 7 },

{ "id": 2, "brand": "BMW", "type" : 5 }

...

]

}

Syncing Data in a Store

To save/synchronize records in a data store with the server, you will have to run the sync() method on the store. It’s also possible to automatically sync the store with the server side. That way, the remote server keeps in close sync with your Sencha Touch app. You can enable this by settingthe autoSync property to true. Although that setting is very easy, it also uses a lot of bandwidth and it’s not possible to batch updates.

More likely, you will use the Ext.data.Store.sync() method to synchronize the store with its proxy programmatically:

store.sync(options);

Before the sync process, Sencha Touch will fire a beforesync system event. When you run the sync() method, all inserts, updates, and deletes are sent as three separate requests, and if you want, you can declare the order in which the three operations should occur. After the sync process, an object is returned with the child objects added, updated, and removed.

For the FindACab app, let’s start by creating a simple store. In Example 9-7, the store has a reference to the Setting model, which has the corresponding fields and a proxy to save the data in the browsers’ Local Storage.

Example 9-7. store/Settings.js

Ext.define('FindACab.store.Settings', {

extend: 'Ext.data.Store',

config: {

model: 'FindACab.model.Setting',

autoLoad: true

}

});

You will also need a SettingsController that’s hooked up to the app.js file, which you accomplish by adding SettingsController to the controllers array. The controller will look like Example 9-8—again, nothing fancy.

Example 9-8. controller/SettingsController.js

Ext.define('FindACab.controller.SettingsController', {

extend: 'Ext.app.Controller',

config: {

models: ['Setting'],

stores: ['Settings']

}

});

Now when you run the FindACab app, the Settings store (which is empty) should be loaded and created. Here’s how I add and sync data to a store. You will manually start to add records to the store.

First, create a reference to the store (you can run this line from the browser dev console):

var store = Ext.getStore('Settings');

The next step is to create some data, a model object that contains the corresponding fields:

var model = Ext.create('FindACab.model.Setting', {

city: 'Amsterdam',

country: 'The Netherlands'

});

Add the data to the store:

store.add(model);

Now comes the magic, the store.sync(). The store and Sencha Touch will make sure the data will persist and get saved to the browsers, Local Storage, through the model:

store.sync()

The preceding line will return an object, with these three arrays:

added

An array with new records added to the store

removed

An array with removed records from the store

updated

An array with edited records in the store

In our case, this code just added one record to the client-side store (see Figure 9-2). We didn’t implement a client-side proxy to save data offline yet, so the data will be gone as soon as you refresh the browser.

Example 9-9 shows the completed code.

Example 9-9. How to add and sync data to a store

var store = Ext.getStore('Settings');

var model = Ext.create('FindACab.model.Setting', {

city: 'Amsterdam',

country: 'The Netherlands'

});

store.add(model);

store.sync();

The result in your browser dev console after adding and syncing a record to the store

Figure 9-2. The result in your browser dev console after adding and syncing a record to the store

You can add records one by one, as you can see in the third step of the previous example. You could also add an array of model objects to the store and then sync it. Whether you add it one by one or add an array with model data, the data looping happens before the sync() call to save performance.

Sometimes you want to get a success response after syncing the store. For example, later in the FindACab app, you will sync a form with user input with the application, and when it’s successful you will reset the markers on a Google Map. As of Sencha Touch version 2.3, it is possible to retrieve a callback after syncing a store. It works very well; you can pass in an options object that makes use of the proxy’s batch method (Ext.data.Batch):

store.sync({

callback: function(batch, options){

console.log(batch);

},

success: function(batch, options){

console.log("succes", batch, options);

},

failure: function(batch){

console.log("error", batch, options);

}

});

Retrieving the created, updated, or deleted records is not so straightforward, because you will work with three different batches:

create

This batch will run after adding new records to the store.

update

This batch will run after editing records in the store.

destroy

This batch will run after deleting records in the store.

If you want, you could request these records via the batches, but what is most important are the success and failure callbacks. We will use these callbacks in our FindACab app later.

Summary

This chapter explained how to load data in a store, how to group a data store, and how to sort and filter data on the client as if on a remote server. At the end of the chapter, I showed you how you can sync a data store with a remote (or client) proxy.

For the FindACab app you have everything set—a configured store and proxy and a model that can be validated. It is just a bit heavy, however: every time I run the FindACab app, it downloads external content from the Internet. When the data doesn’t change often, it’s better to store the data in the app. The app doesn’t need to be connected to the Internet and it loads much faster, because it doesn’t need to download. In the next chapter, then, we will discuss how to save and load data offline.