Chapter 8. The Flickr Finder Application

So far, we have looked at Sencha Touch components individually or in small, simple applications. In this chapter, we are going to create a well-structured and more detailed application, using Sencha Touch. This will include:

  • An introduction to the Model View Controller (MVC) design pattern

  • Setting up a more robust folder structure

  • Setting up the main application files

  • Using the Flickr API

  • Registering components

  • Setting up the SearchPhotos component

  • Setting up the SavedPhotos component

  • Adding the finishing touches to publish the application

The basic application

The basic idea for this application will be to use the Flickr API to discover photos taken near our location. We will also add the ability to save interesting photos we might want to look at late.

When you are first creating an application, it's always a good idea to sketch out the interface. This gives you a good idea of the pieces you will need to build and also allows you to navigate through the various screens the way a user would. It doesn't need to be pretty; it just needs to give you a basic idea of all the pieces involved in creating the applicatin.

Aim for something very basic, such as this:

The basic application

Next, you will want to tap your way through the paper interface, just like you would with a real application, and think about where each tap will take the user, what might be missing, and what might be confusing for the user.

Our basic application needs to be able to display a list of photos as well as a close-up of a single photo. When we tap a photo in the list, we will need to show the larger close-up photo. We will also need a way to get back to our list when we are finished.

When we see a photo we like, we need to be able to save it, which means we will need a button to save the photo as well as a separate list of saved photos and a close-up single view for the saved photo, as well.

Once we are happy with the drawings, we can start putting together the code to make our paper mock-up into something like this:

The basic application

Introduction to Model View Controller (MVC)

Before we get started building our application, we should spend some time talking about structure and organization. While this might seem like a boring detour into application philosophy, it's actually one of the most critical considerations for your application.

First consider the monolithic application, with everything in one enormous file. It seems crazy, but you will encounter hundreds of applications that have been coded in just this fashion. Attempting to debug something such as this is a nightmare. Imagine finding the missing closing curly brace inside of a component array 750 lines long. Yuck!

The question then becomes one of how to break up the files in a logical fashion.

Model View Controller, or MVC, organizes the application files based on the functionality of the code:

  • Models describe your data and its storage

  • Views control how the data will be displayed

  • Controllers handle the user interactions by taking input from the user and telling the views and model how to respond, from the user's input

This means each part of your application will have separate files for each of these parts. Let's take a look at how this is strctured:

Introduction to Model View Controller (MVC)

Our css folder contains our local style sheets and our lib folder contains our Sencha Touch library files, just as before, but we have some new folders named models, controllers, and views, inside our a pp folder.

Our model files will contain code for creating our models and the stores that will contain our data. There will be one model file for each of our different datatypes (we will talk about how to split out the datatypes in the next setion).

Our controller files will contain most of the functionality for the application: loading the data into the store, getting the data back out for display, and listening for any input from the user. These controller files will also be split into separate files for each type of data that we deal with.

The views folder will contain all of our display information for each of our data pieces. Since we will likely have multiple views for each of our datatypes (for example, a form and a list), we will probably split these views out into separate sub folders, one per datatype.

By splitting the files out this way, it is much easier to reuse code across applications. For example, let's say you build an application that has a model, controller, and views for a user. If you want to create another application that needs to deal with users, you can simply copy over the individual files for the model, views, and controller into your new application. If all the files are copied over, then the user code should work just as it did in the previous application.

If we build a monolithic application, you would have to hunt though the code and grab out bits and pieces, and reassemble them in the new application. This would be a slow and painful process. By separating our components by functionality, it's much easier to reuse code between projects.

Building the foundation

Before we can build the application, we need to set up our HTML file that will link to the rest of our files and serve as the overall container for our application:

<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Flickr Findr</title>

<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0; maximum-scale=1.0; user-scalable=0;" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<link rel="apple-touch-icon" href="apple-touch-icon.png" />

<link rel="stylesheet" href="lib/resources/css/sencha-touch.css" type="text/css">
<link rel="stylesheet" href="css/flickrfindr.css" type="text/css">
</head>
<body>
<script type="text/javascript" src="lib/sencha-touch-debug.js"></script>

<div id="sencha-app">
<script type="text/javascript" src="app/app.js"></script>

<!-- Place your view files here -->
<div id="sencha-views">

</div>

<!-- Place your model files here -->
<div id="sencha-models">

</div>

<!-- Place your controller files here -->
<div id="sencha-controllers">

</div>
</div>
</body>
</html>

This basic HTML file setup links to all of our various JavaScript files and the Sencha Touch framework. In the body of the index.html file, we have also created three sections for our model, view, and controller files. As you create each file, you will need to add a link to the file in the appropriate section o the index file.

Tip

Placing models, views, and controllers in the page body

In a typical HTML page, JavaScript is placed in the <head></head> tags. When the browser loads that page, it must load everything in the head tag before loading the rest of the page. Once the head tag is fully loaded, any HTML within the <body></body> tags gets rendered, and any files in the body tags are loaded serially. By moving our components inside the <body></body> tag, we can load the pieces the user will see first, at the top of our list. This leads to a slightly quicker load time from the user's perspective.

Next, we need a way to launch the initial application, and a basic structure where we can place our different data views for display.

This foundation begins with two files: one called viewport.js, in the views folder, and another called app.js, in the main app folder. Let's take a look at these files.

The code for our app.js file is pretty simple:

FlickrFindr = new Ext.Application({
defaultTarget: "viewport",
name: "FlickrFindr",
launch: function() {
this.viewport = new FlickrFindr.Viewport();
    }
});
Ext.namespace('FlickrFindr.view', 'FlickrFindr.model', 'FlickrFindr.store', 'FlickrFindr.controller'),

This is the file that initially declares our viewport method and launches our application. We also create the initial namespace for our models, views, and controllers. As we mentioned back in Chapter 2, Creating a Simple Application, namespace makes sure that when I call FlickrFinder.Viewport(), I don't end up getting the generic xt.Viewport instead.

Tip

Namespace bug

There is currently a namespace bug in Sencha Touch 1.1, in the Ext.Application setup. Currently, as stated in the documentation, when the application is created, Ext calls the ns function to create the namespaces we need for our application. Unfortunately, the function ns does not actually exist in version 1.1 of Sencha Touch, so the namespaces are not created automatically. The upshot of this is that we have to create them manually (no matter what the Sencha Touch 1.1 documentation might tell you).

The launch function creates our new FlickrFindr.Viewport() method, which we will define in our viewport.js file.

As before, our viewport is simply an extension of a standard Ext.Panel component. In the viewport.js file, add the following:

FlickrFindr.Viewport = Ext.extend(Ext.Panel, {
layout    : 'card',
fullscreen: true,

initComponent: function() {
Ext.apply(this, {
items: [
{ xtype: searchphotos }
]
        });

FlickrFindr.Viewport.superclass.initComponent.apply(this, arguments);
    }
});

This viewport will be the skeleton of our application, which will hold our other components. However, unlike our previous examples, the bulk of our individual component code is going to live in separate files. Our items section lists a single component with an xtype attribute of searchphotos. We will create this component in the The SearchPhotos component section.

Splitting up the pieces

The next thing we need to consider is how our application gets split into our separate MVC pieces. For example, if your application tracks people and what car they own, you would likely have a model and controller for the people, and a separate model and controller for the cars. You would also likely have multiple views for both cars and people, such as add, edit, list, details, and so on.

In our application, we will be dealing with two different types of data. The first is our search data for our photos and the second is our saved photos.

If we break this down into models, views, and controllers, we get something such as the following:

Splitting up the pieces

Our controllers are separated out by functionality for saved photos and search photos.

Since they are dealing with the same type of data, each of our controllers can use the same model, but they will need different stores, since they're each using different actual data sets. Our data stores will be part of the model file, so we have left the two models as separate blocks in our diagram (since they will still be separate files).

For views, our search needs a list view for Search Photos and a Photo Details view. The saved photos will also need a view for the list of saved photos and a view for editing/adding the saved photos.

Tip

Naming conventions

There are a few naming conventions when you use an MVC structure. While they are not required, they are strongly recommended. The conventions will make it easier to understand for anyone else who has to work with your code. The controller is typically a plural word or words. A model is a singular version of the controller name. Finally, the default view for the controller should be named the same (remember, the models, views, and controllers are in separate folders). This will make it clear which pieces belong together within your code.

Now that we have an idea of how our application needs to be laid out, we have one last task to perform before we get started. We need to get an API key from Flickr.

Using the Flickr API

A majority of popular web applications have made an API (Appli cation Programming Interface) available for use in other applications. This API works in much the same way as our Sencha Touch framework. The API provides a list of methods that can be used to read from, and even write data to, the remote server.

These APIs typically require a key in order to use them. This allows the service to keep track of who is using the service and curtail any abuses of the system. API keys are generally free and easy to acquire.

Go to the Flickr API site, http://www.flickr.com/services/api/, and look for the phrase API Keys. Follow the link and apply for an API key, using the form provided. When you receive your API key, it will be a 32-character long string composed of numbers and lowercase letters.

Each time you send a request to the Flickr API server, you will need to transmit this key as well. We will get to that part a bit later.

The Flickr API covers a little over 250 methods. Some of these require you to be logged in with a Flickr account, but the others only require an API key.

For our purposes, we will be using a single API method called flickr.photos.search, which requires no login. This method looks for photos, based on some criteria. We will be using the current latitude and longitude of the device to get back photos within a specified distance from our current location.

Our search results come back to us as a big bundle of JSON that we will need to decode for display.

Once you have the API key, we can begin setting up our models, views, and controllers.

The SearchPhotos component

We will start building with our search component. To begin with, we need to add links in our main index.html to the files we will be creating. If you remember from the beginning of the chapter, we left ourselves some placeholders for adding in our models, views, and controllers. Let's add those in now, before we create the actual files:

<!-- Place your view files here -->
<div id="sencha-views">
<script type="text/javascript" src="app/views/Viewport.js"></script>
<script type="text/javascript" src="app/views/SearchPhotos.js"></script>
<script type="text/javascript" src="app/views/PhotoDetails.js"></script>
</div>

<!-- Place your model files here -->
<div id="sencha-models">
<script type="text/javascript" src="app/models/SearchPhoto.js"></script>
</div>

<!-- Place your controller files here -->
<div id="sencha-controllers">
<script type="text/javascript" src="app/controllers/SearchPhotos.js"></script>
</div>

The sencha-views section has our main viewport, our SearchPhotos (list) view, and our PhotoDetails view.

Our sencha-models section contains our SearchPhoto model that will be used by all our views.

The sencha-controllers section contains our single SearchPhoto controller that will handle communication between our views and models.

Now that these links are in place, we can start building the actual files.

The best place to start with new components is the model. Typically, if we understand the data we need to store and display, we can use that to determine how the rest of the application should be built.

The SearchPhotos model

Our search results will be constrained, in part, by the data we can get back from the Flickr API. However, we also want to display the images as part of our search results. This means we need to look at the Flickr API and see what is required to display an image from Flickr in our application.

If we take a look at http://www.flickr.com/services/api/misc.urls.html, we see that Photo Source URLs in Flickr has the following structure:

http://farm{farm-id}.static.flickr.com/{server-id}/{id}_{secret}.jpg

This means that, in order to display each photo, we need:

  • farm-id: The group of servers the image is on

  • server-id: The specific server the image is on

  • id: The unique ID for the image

  • secret: A code used by the Flickr API to route requests

These are all things that we get back as part of our flickr.photos.search request. We also get back the title for the photo, which we can use as part of our display.

Given these criteria, we need a SearchPhotos.js file in our models folder, with the following code:

Ext.regModel('FlickrFindr.model.SearchPhoto', {
fields: [
    {
name: 'id',
type: 'int'
  },
    {
name: 'owner',
type: 'string'
  },
    {
name: 'secret',
type: 'string'
  },
    {
name: 'server',
type: 'int'
  },
    {
name: 'farm',
type: 'int'
  },
    {
name: 'title',
type: 'string'
  }
  ]
});

We register our model, just as before, and then declare which fields we are using.

Tip

Remote loading

It's a good idea to get into the habit of using the full namespace of FlickrFindr.model.SearchPhoto. The next version of Sencha Touch will support remote loading for components. This means that you will not need to include all the files as part of your index. Sencha Touch will grab the component files and load them only when needed. It will do this based on the full name; the model is part of our main Flickr Finder application in the models folder and it's called SearchPhoto.js.

Next, we need to add some code to our SearchPhoto.js file, in the model folder. Beneath the model attribute, we need to add the following:

Ext.regStore('FlickrFindr.store.SearchPhotos', {
model: 'FlickrFindr.model.SearchPhoto',
autoLoad: false,
proxy: {
type: 'scripttag',
callbackParam: 'jsoncallback',
url: 'http://api.flickr.com/services/rest/', 
extraParams: {
      'method': 'flickr.photos.search',
      'api_key': '783f66a1146d0be1ee5975785e6eb7a7',
      'format': 'json',
      'per_page': 25
    },
reader: {
type: 'json',
root: 'photos.photo'
    }
  }
});

Here, we register the FlickrFindr.store.SearchPhotos store, the same way we registered a model. We are using the scripttag proxy.

If you remember from Chapter 6, Getting Data In, this proxy type is used for handling requests to a separate server, much like JSONP. These cross-site requests require a callback function in order to process the data returned by the server. However, unlike JSONP, the scripttag proxy will handle the callback functionality for us almost automatically.

We say almost, because Flickr's API expects to receive the callback variable as:

jsoncallback =a_really_long_callback_function_name

By default, the store sends this variable as:

callback =a_really_long_callback_function_name

Fortunately, we can change this by setting this configuration option:

callbackParam: 'jsoncallback'

The next section sets the URL for contacting the Flickr API, which is url: 'http://api.flickr.com/services/rest/'. This URL is the same for any requests to the Flickr API. The extraParams setting is the piece that actually tells the API what to do. Let's take a closer look at that piece:

extraParams: {
      'method': 'flickr.photos.search',
      'api_key': 'your-api-key-goes-here',
      'format': 'json',
      'per_page': 25
    }

The extraParams are a set of keys and values that are posted to the URL. Notice that, unlike the configuration options, extraParams have both sides of the : in quotes. This can trip you up if you forget.

In this case, Flickr's API needs the following information:

  • method: The method we are calling

  • api_key: Our own personal API key (the one in the example is fake; you will need to supply your own API key in order for this to work)

  • format: This is how we want to get the information back

  • per_page: This sets how many images we want to get back from our request

Once we get our data back, we pass it to the reader:

reader: {
type: 'json',
root: 'photos.photo'
    }

Since we set 'format': 'json', we need to set type: 'json' in our reader function, We also need to tell the reader function where to start looking for photos in the json array that gets returned from Flickr. In this case, root: 'photos.photo' is the correct value.

Now that we have our data model and store set up, we need two views: the SearchPhotos view and the PhotoDetails view.

The SearchPhotos view

Create a SearchPhotos.js file in our views folder. This will be the first of our two views. Each view represents a single Sencha Touch display component. In this case, we will be using an Ext.Panel class for display and an XTemplate to lay out the panel.

Our XTemplate looks as follows:

FlickrFindr.view.SearchPhotoTpl = new Ext.XTemplate(
'<div class="searchresult">',
'<img src="{[this.getPhotoURL("s", values)]}" height="75" width="75"/>',
' {title}</div>',
{
getPhotoURL: function(size, values) { 
size = size || 's';
var url = 'http://farm' + values.farm + '.static.flickr.com/' + values.server + '/' + values.id + '_' + values.secret + '_' + size + '.jpg';
return url;
  }
});

The first part of our XTemplate supplies the HTML we are going to populate with our date. We start by declaring a div tag with a class of searchresult. This gives us a class we can use later on to specify which photo result is being tapped.

Next, we have an image tag, which needs to include a Flickr image URL for the photo we want in the list. We could assemble this string as part of the HTML of our XTemplate, but we are going to take the opportunity to add some flexibility, by making this into a function on our XTemplate.

Flickr offers us a number of sizing options when using photos in this way. We can pass any of the following options along as part of our Flickr image URL:

  • s: Small square, 75x75

  • t: Thumbnail, 100 on longest side

  • m: Small, 240 on longest side

  • -: Medium, 500 on longest side

  • z: Medium, 640 on longest side

  • b: Large, 1024 on longest side

  • o: Original image, either a JPG, GIF, or PNG depending on source format

We want to set our function up to take one of these options along with our template values and create the Flickr image URL. Our function first looks to see if we were passed a value for size, and if not, we set it to s, by default, using size = size || 's';.

Next, we assemble the URL using our XTemplate values and the size. Finally, we return the URL for use in our XTemplate HTML. This will let us create a thumbnail for each of our images.

Now, we need a place to put the template and our images

FlickrFindr.view.SearchPhotos = Ext.extend(Ext.Panel, {
id: 'searchphotos',
layout: 'card',
fullscreen: true,

initComponent: function() {
Ext.apply(this, {
dockedItems: [],
items: [
        {
xtype: 'list',
store: 'FlickrFindr.store.SearchPhotos',
itemTpl: FlickrFindr.view.SearchPhotoTpl
      }
      ]
    });

FlickrFindr.view.SearchPhotos.superclass.initComponent.apply(this, arguments);
  }
});

Ext.reg('searchphotos', FlickrFindr.view.SearchPhotos);

We create our FlickrFindr.view.SearchPhotos model by extending the Ext.Panel class. This panel will have a card layout, so we can switch between our list of photo thumbnails and a details page.

The initComponent configuration will set our dockedItems and items components for the panel. To start with, we only have a single list component, which uses our store and itemTpl objects, that we created previously.

The last two lines initialize the component and then register our new xtype attribute of the searchphotos component.

At this point, our application doesn't do much of anything, because the store isn't loading anything. We also have to tell the store our current location in order to get the photos nearby. It would also be nice if the application did this when it started up.

In order to accomplish these goals, we need to add a listener on our list component (after the items section):

listeners: {
render: function() {
var dt = new Date().add(Date.YEAR, -1);
var geo = new Ext.util.GeoLocation({
autoUpdate: false
      });
geo.updateLocation(function(geo) {
var easyparams = {
          "min_upload_date": dt.format("Y-m-d H:i:s"),
          "lat": geo.latitude,
          "lon": geo.longitude,
          "accuracy": 16,
          "radius": 10,
          "radius_units": "km"
        };
this.getStore().load({
params: easyparams
        });
      }, this);
    }
}

Here, we have added a listener for the render function. This will fire once when the application starts.

In order to make sure we only get recent photos, we create a new date object that will hold the date one year ago, (new Date().add(Date.YEAR, -1);, for us to use later on.

We also set up a new GeoLocation object using the following:

var geo = new Ext.util.GeoLocation({
autoUpdate: false
      });

By setting autoUpdate: false, we only get the location data once. This will keep us from beating the user's battery to death by constantly updating our location.

As we have turned autoUpdate off, we need to manually trigger and update using geo.updateLocation() and pass it a function to run. The first thing our function does is to set up an array of Flickr API parameters we can then pass on to our store:

var easyparams = {
 "min_upload_date": dt.format("Y-m-d H:i:s"),
 "lat": geo.latitude,
 "lon": geo.longitude,
 "accuracy": 16,
 "radius": 10,
 "radius_units": "km"
};

The store takes anything we define as params and transmits it as a set of POST variables, as part of the load request. In this case, we send the parameters to Flickr's API and Flickr returns photos based on these variables.

The first parameter sets the minimum date for the photos we are interested in seeing. The other parameters set our current location via latitude and longitude from our GeoLocation object.

The accuracy parameter uses a range of 1 (World Level) to 16 (Street Level) and we have set this to 16. We are also setting a radius of 10 km for our search. We will play around with these elements later, in our advanced search.

Once all of our parameters are set, we still have one last thing to add, the SearchPhotos controler.

The SearchPhotos controller

The controller is where the bulk of our action code will go. Make a new file called SearchPhotos.js and put it in the controllers folder. Add the following code to the file:

Ext.regController('searchphotos', {
showResults: function() {
var results = Ext.getCmp('searchphotos'),
results.setActiveItem(0);
  }
});

This code registers our controller and the first function sets the active item on our searchphotos container to our list (item 0).This function will be used as part of our Back button, later in the code. Let's give it a try in Safari.

If we load up our application in Safari, we will first get an alert asking if Safari can use our current location:

The SearchPhotos controller

This alert allows users to decline the application access to their current location and ensures user privacy.

If you click Allow, our list will render and begin loading photos.

The SearchPhotos controller

You should now see a list of photos near your location.

Tip

Flickr API explorer

If you are not getting back any results, you might want to try the Flickr API tester web page. This page will let you enter your parameters into a web form and see what you get back from the flickr.photos.search API request http://www.flickr.com/services/api/explore/flickr.photos.search. This will let you know if you are simply having a code issue, or if nobody has actually taken photos in your area.

Now that we have our photos working, it would be nice to see them in a larger format, so let's add our details view.

Adding the PhotoDetails view

First, we need to add the tap handler to our current list in the views/SearchPhotos.js file. This handler will swap our card layout to the details view, when an item in the list i tapped.

Underneath our listener for the render event, let's add one for tap handling:

itemtap: function(list, item) {
var photo = list.getStore().getAt(item);

Ext.dispatch({
controller: 'searchphotos',
action: 'showDetails',
args: [photo]
  });
}

As part of our function, we are passed the item number of the photo that was tapped. We need the actual data record from the store in order to display the details. We do this with var photo = list.getStore().getAt(item).

Next, we use a method called Ext.Dispatch(). This method allows us to send commands and arguments back to the controller. In this case, we are calling showDetails and passing the photo record from the store.

The last thing we need to do in this file is add our details component into our items list. After the list component, add the following:

{
xtype: 'photodetails'
}

This adds a new component with an xtype attribute of photodetails. We will create this view after we add the showDetails code to our controller. We should be done with the views/SearchPhotos.js file for now. Let's move back to our controller file.

In the controller/SearchPhotos.js file, we need to add the code to display our photo in the PhotoDetails view (don't worry, we'll create that next). We can add the following new function after the showResults function:

showDetails: function(interaction) {
var photo = interaction.args[0];
var results = Ext.getCmp('searchphotos'),
results.down('photodetails').update(photo.data);
results.setActiveItem(1);
}

For this function, we have been passed the photo record as part of an array of arguments, so we grab it with var photo = interaction.args[0]. Next, we get our searchphotos component and use the down method to find our photodetails item (which was part of the list of items in the searchphotos component). We then load the photoDetails with our photo data. Now, we can switch the card layout to show our details, using results.setActi eItem(1).

Now that our controller understands what to do with the photo it's receiving from our tap event, we need to create the PhotoDetails view that will actually display the photo. This file should be placed in the views folder.

Our PhotoDetails view looks as follows:

FlickrFindr.view.PhotoDetails = Ext.extend(Ext.Panel, {
id: 'photodetails',
fullscreen: true,
tpl: '<h1>{title}</h1><img src="http://src.sencha.io/x100/x100/http://farm{farm}.static.flickr.com/{server}/{id}_{secret}_b.jpg"></img>',
dockedItems: [
    {
xtype: 'toolbar',
items: [
      {
text: 'Back',
ui: 'back',
handler: function() {
Ext.dispatch({
controller: 'searchphotos',
action: 'showResults'
        });
      }
    }
    ]
  }
  ],
initComponent: function() {
FlickrFindr.view.PhotoDetails.superclass.initComponent.apply(this, arguments);
  }
});

Ext.reg('photodetails', FlickrFindr.view.PhotoDetails);

We start this file out much like our SearchPhotos view. We extend panel and give it an id and a tpl component.

We create the image link as part of the template, instead of adding it in as a function, as we did in the previous SearchPhotostpl model. This is simply to show that either way will work just fine. In this tpl component, we also added a reference to Sencha.io to resize our image based on the device:

http://src.sencha.io/x100/x100/http://farm{farm}.static.flickr.com/{server}/{id}_{secret}_b.jpg

By using x100/x100, we can automatically resize the image to the full screen size of whatever device we run it on.

Next, we set up our dockedItems component with a Back button, so we can return to the list of photos. This button uses Ext.Dispatch to call the showResults function we added previously for the controller (the one that sets our card layout back to the list view).

Finally, we initialize our component and register our new xtype attribute, the same way we did with the SearchPhotos view.

Once we have the code for our view in place, we should be able to see our details by clicking on a file in the list.

Adding the PhotoDetails view

Now that we can view our photos at full size, let's set up a savedphoto component that will allow us to save a link to any photos we like.

The savedphoto component

Our savedphoto component will need to store the information for a single photo from our search results. We will also need a list view for our saved photos and a details view, just like our previous SearchPhotos and PhotoDetails models.

Since our savedphoto model is simply displaying a subset of all of our photos, we can reuse a considerable amount of our code for this part of the application.

The SavedPhoto model

Since our SavedPhotos and our SearchPhotos models are storing the exact same type of data, we don't need to create a new model. However, we do need a separate data store, one that will store our SavedPhotos model locally.

Let's add a SavedPhotos.js file to our models folder:

Ext.regStore('FlickrFindr.store.SavedPhotos', {
model: 'FlickrFindr.model.SearchPhoto',
autoLoad: true,
proxy: {
type: 'localstorage',
id: 'flickr-bookmarks'
  }
});

Here, we just register our FlickrFindr.store.SavedPhotos class and reuse our model from FlickrFindr.model.SearchPhoto. We also want this store to load up when the application launches. Since it is grabbing local data, this should not present a huge load for the application.

We set our proxy to store the data locally and assign the store an id component, flickr-bookmarks, so we can grab it later.

Once you are finished with the models/SavedPhotos.js file, make sure to link to it in the index file.

The SavedPhoto views

For the SavedPhoto views, we need a list and a detail view. These views will be very close to what we already have for our SearchPhotos and PhotoDetails models. In fact, we can start by making copies of those two files and tweaking our layouts a bit.

In the views folder, make a copy of SearchPhotos.js and rename it to SavedPhotos.js. You will also need to replace all the occurrences of SearchPhotos and searchphotos, with SavedPhotos and savedphotos, respectively (remember that JavaScript is case-sensitive). Your code should ok as follows:

FlickrFindr.view.SavedPhotos = Ext.extend(Ext.Panel, {
id: 'savedphotos',
layout: 'card',
fullscreen: true,

initComponent: function() {
Ext.apply(this, {
dockedItems: [{
xtype: 'toolbar',
dock: 'top',
title: 'Saved Photos',
items: []
      }],
items: [
        {
xtype: 'list',
store: 'FlickrFindr.store.SavedPhotos',
itemTpl: FlickrFindr.view.SearchPhotoTpl,
listeners: {
itemtap: function(list, item) {

var photo = list.getStore().getAt(item);

Ext.dispatch({
controller: 'savedphotos',
action: 'showDetails',
args: [photo]
            });
          }
        }
      },
        {
xtype: 'savedphotodetails'
      }
      ]
    });

FlickrFindr.view.SavedPhotos.superclass.initComponent.apply(this, arguments);
  }
});

Ext.reg('savedphotos', FlickrFindr.view.SavedPhotos);

You will notice that we did not include a template in this file; we are just reusing our FlickrFindr.view.SearchPhotoTpl class from the SearchPhotos.js file. It is perfectly fine to create a separate template, but reusing saves us a bit of memory and time.

Other than that, the file is largely the same as our SearchPhotos.js file: We create a panel with a card layout and add a toolbar. We have two items in the card layout: a list and a details panel (which we will create next). We set up our itemTap event to contact the controller and fire the showDetails function. Finally, we initialize the component and register an xtype attribute of savedphotos, for the component.

Note

While it might seem a bit redundant to have two files that are so similar, it should be noted that they both read from different data stores, and they need to be addressed differently by the controllers. We are also going to make a few tweaks to the look of our different views, before it's all over.

For our SavedPhotoDetails model, we will take a similar approach. Copy the PhotoDetails.js file to your views folder and rename it to SavedPhotoDetails.js. This file will display a single saved photo. However, unlike the details for our search photos, this saved photo details panel does not need a Save button.

You will need to modify the file to remove the Save button:

FlickrFindr.view.SavedPhotoDetails = Ext.extend(Ext.Panel, {
id: 'savedphotodetails',
fullscreen: true,
tpl: '<h1>{title}</h1><img src="http://src.sencha.io/x100/http://farm{farm}.static.flickr.com/{server}/{id}_{secret}_b.jpg"></img>',
dockedItems: [
    {
xtype: 'toolbar',
items: [
      {
text: 'Back',
ui: 'back',
handler: function() {
Ext.dispatch({
controller: 'savedphotos',
action: 'showSavedPhotos'
        });
      }
    }
    ]
  }
  ],
initComponent: function() {
FlickrFindr.view.SavedPhotoDetails.superclass.initComponent.apply(this, arguments);
  }
});

Ext.reg('savedphotodetails', FlickrFindr.view.SavedPhotoDetails);

As before, this is much the same as the PhotoDetails file we created earlier; we have switched the names and changed our Back button to show our SavedPhotos list instead of the main photos list.

When you are finished with the two views, add them into the sencha-views class of our index.html, thus:

<div id="sencha-views">
<script type="text/javascript" src="app/views/Viewport.js"></script>
<script type="text/javascript" src="app/views/SearchPhotos.js"></script>
<script type="text/javascript" src="app/views/PhotoDetails.js"></script>
<script type="text/javascript" src="app/views/SavedPhotos.js"></script>
<script type="text/javascript" src="app/views/SavedPhotoDetails.js"></script>
</div>

Now, we can move on to the controller for our savedphotos component.

The SavedPhotos controller

Create a new file called SavedPhotos.js in our controller folder. This file will have a structure similar to that of our other controller file; first we register the controller, and then we add functions:

Ext.regController('savedphotos', {

showDetails: function(interaction) {
var photo = interaction.args[0];
var savedPhotos = Ext.getCmp('savedPhotos'),
savedphotos.down('savedphotodetails').update(photo.data);
savedphotos.setActiveItem(1, 'slide'),
  },
showSavedPhotos: function() {
var savedPhotos = Ext.getCmp('savedPhotos'),
savedPhotos.setActiveItem(0, {
type: 'slide',
direction: 'right'
    });
  }

});

The first function, showDetails, is passed an array, called interaction, from our tap event (even though the user only taps one item, it is still passed as part of an array). We then grab our savedphotodetails component, by using the down method to search by id, and update the content area, using the data from the photo. Finally, we set the active item to 1, which is our savedphotodetails component, and animate the change using the slide animation.

If you remember, our showSavedPhotos function is tied to the Back button on our savedphotodetails component. This function selects the card layout for our main savedphotos panel (using Ext.getCmp('savedphotos')) and sets the active item back to 0, returning it to the savedphotos list.

Now, we need to add one more function to our controller. This one will allow us to pop up an alert when the user saves a photo and will ask them to name the photo. Since we only need a single text field, we probably don't need to create a separate form view; we can just use the Ext.Msg component.

Above the showDetails function, we need to add the following code:

addSavedPhoto: function() {
var panel = Ext.getCmp('photodetails'),

Ext.Msg.prompt('Save Photo', 'Please enter a description:', function(btn, value) {
if (btn == 'ok') {
var savedPhotoStore = Ext.StoreMgr.get('FlickrFindr.store.SavedPhotos'),

var savedPhoto = Ext.ModelMgr.create(panel.data, 'FlickrFindr.model.SearchPhoto'),
savedPhoto.set('title', value);
savedPhotoStore.loadRecords([savedphoto], true); 
savedPhotoStore.sync(); 

var tabPanel = Ext.getCmp('viewport'),
tabPanel.setActiveItem(1); //switch to the savedphoto view.
    }
  }, this, true, //multiline
panel.data.title, // value
  {
focus: true,
autocorrect: true,
maxlength: 255
  });
}

Our addSavedPhoto function first grabs the current photo details panel. This gives us access to all of the data currently stored in the panel.

Then the function shows off some of the power of the simple Ext.Msg component. Let's list out what we have here, before moving in for a closer look. First, by declaring Ext.Msg.prompt, we tell the message box that we are prompting the user to give us some information in a text field. Then, the Ext.Msg component sets the following:

  • A title for the pop up

  • The text for the pop up

  • The function that received the button that got pressed, and the value of our text field

  • A scope for the function (this)

  • The value true (right after scope is set to this), which makes the text field capable of multiple lines

  • A value to set as the default for our text field

  • focus, autocorrect, and maxlength, which are three of the configuration options for the prompt configuration

The title and text are pretty straightforward, but let's take a closer look at the function. The function is called when the user clicks any of the buttons on the message dialog. The function is passed the name of the button that was pressed, and in the case of a prompt, the value of the text field.

To process this information and get it into our data store, we first grab the store using:

var savedPhotoStore = Ext.StoreMgr.get('FlickrFindr.store.SavedPhotos'),

Next, we create a new savedphoto component (FlickrFindr.model.SearchPhoto), using the model manager, and fill the data in with our current panel data (this is our current photo data). We also set the title to match the value the user entered into the message field:

var savedPhoto = Ext.ModelMgr.create(panel.data, 'FlickrFindr.model.SearchPhoto'),
savedphoto.set('title', value);

Once this is complete, we load the new savedphoto component in and sync the store to save our data:

savedPhotoStore.loadRecords([savedphoto], true);
savedPhotoStore.sync();

Once we are finished, we grab our main viewport and switch back to our savedphotos list:

var tabPanel = Ext.getCmp('viewport'),
tabPanel.setActiveItem(1);

The rest of our Ext.Msg.prompt code sets the configuration options for the message box, providing the function scope, setting our text field to be multiline, giving a default value for our text area, and adding some additional configuration options.

This last group of values is called promptConfig and it's an optional set of configurations for the text area of the message box. Ours sets the focus on the text area (when the box appears), turns on auto-correct, and sets a maximum text length of 255 characters.

Tip

Multiline bug

There is currently a bug in Sencha Touch 1.1 if multiline is set to true. The bug causes the maximum length of the field to default to 0, if you are using the Safari or Chrome browsers. The workaround is to set the maxlength to an actual number in the prompt configuration.

When you are finished with the controller code, remember to link to it in the index.html file:

<div id="sencha-controllers">
<script type="text/javascript" src="app/controllers/SearchPhotos.js"></script>
<script type="text/javascript" src="app/controllers/SavedPhotos.js"></script>
</div>

Now that we are done with the savedphotos controller, we can add the savedphotos component into our viewport.

Adding SavedPhotos to the viewport

When our viewport started out, we only had one item, the SearchPhotos component. Now that we have two separate lists, a tab panel would make more sense. Let's change the viewport.js code to look like this:

FlickrFindr.Viewport = Ext.extend(Ext.TabPanel, {
id: 'viewport',
fullscreen: true,
cardSwitchAnimation: 'slide',
tabBar: {
dock: 'bottom',
layout: {
pack: 'center'
    }
  },
initComponent: function() {
Ext.apply(this, {
items: [{
xtype: 'searchphotos',
title: 'Search',
iconCls: 'search'
      },
    {
xtype: 'savedPhotos',
title: 'Saved Photos',
iconCls: 'favorites'
      }]
    });

FlickrFindr.Viewport.superclass.initComponent.apply(this, arguments);
  }
});

The first change we made was to swap out Ext.Panel for Ext.TabPanel, in our extend function.

Since the TabPanel needs a cardSwitchAnimation component for switching the tabs, and a tabBar component for showing the tabs, we added those as well.

Next, we added our panels for searchphotos and savedphotos, along with titles and an iconCls attribute for each. This will show up as part of our tabs at the bottom of the application.

The last thing we need to do is add our Save button, so that the user can save a specific photo.

Adding the Save button

The Save button needs to appear when the user is looking at a specific photo. This means we need to add it to our PhotoDetails.js view.

In the views folder, open the PhotoDetails.js file. Currently, our dockedItems component only has a Back button. We want to add a Save button on the right-hand side of the toolbar:

dockedItems: [
  {
xtype: 'toolbar',
items: [
    {
text: 'Back',
ui: 'back',
handler: function() {
Ext.dispatch({
controller: 'searchphotos',
action: 'showResults'
      });
    }
  }, {
xtype: 'spacer'
  },
    {
text: 'Save',
ui: 'action',
handler: function() {
Ext.dispatch({
controller: 'savedPhotos',
action: 'addSavedPhoto'
      });
    }
  }
  ]
}
]

We have actually added two items to our toolbar; the first one is a spacer component. The spacer component is a specialty toolbar component that shifts every item after the spacer over to the right side of the toolbar.

The second item is our Save button. This button's handler uses the dispatch function to tell our controller to run the addSavedPhoto fuction.

Once this code is added and saved, our application should be ready t use.

Adding the Save button

Polishing your application

Now that we've finished our application, we will want to add some finishing touches to really make our application shine and add a level of professionalism to the completed product. The good news is that all of these are easily and quickly implemented.

Animated transitions

One thing you'll notice is that, when we go from our SearchPhotos list view to the PhotoDetails view, we just jump from one to another via setActiveItem(). It can be a little jarring. In our SavedPhotos views, however, we snuck in some animations as the second argument to the setActiveItem() call. Going back and adding those same animations to our SearchPhotos controller will not only make the behavior more consistent, but it'll also make for a cleaner-feeling interface.

In the controllers/SearchPhotos.js file, find the showDetails function and change the following line:

results.setActiveItem(1);

Change it to:

results.setActiveItem(1, 'slide'),

The 'slide' animation will slide the PhotoDetails card in from the right, while sliding the SearchPhotos list out to the left. When we go back to the SearchPhotos list from the PhotoDetails view, we want to slide in the other direction. That takes a bit more configuration. Find the showResults function in the same controller file and change the following line:

results.setActiveItem(0);

Change it to:

results.setActiveItem(0, {
type: 'slide',
direction: 'right'
});

This will slide everything out to the right and in from the left, reversing the direction when we first went to our PhotoDetails view. There are more settings and animation types listed in the documentation, under Ext.Anim.

When you go from one web page to another, the new page simply replaces the old. But, in most mobile applications, moving from one view to another involves an animation. These sorts of animated transitions are easy to add and are important, because they help distinguish your application and make it feel more organic than a run-of-the-mill web page.

Application icons

As mentioned back in Chapter 1, Let's Begin with Sencha Touch!, users can navigate to your web application and then choose to save it to the desktop of their mobile device. When someone installs your application in this fashion, you can specify which icon is displayed on his or her home screen.

Application icons

We've already got the code for this in our index.html file:

<link rel="apple-touch-icon" href="apple-touch-icon.png" />

Even though this says "apple-touch-icon", most mobile devices, including Android devices, recognize the tag. Apple recommends that your application icon be 57 x 57 px, for some devices, and 114 x 114 px, for newer devices. It's safest to create your icon at a larger size, as it will be automatically scaled down, if necessary. Additionally, on Apple iOS devices, the corners will be automatically rounded and a glossy effect will be added.

If you want your icon left as it is, you can use the following tag:

<link rel="apple-touch-icon-precomposed" href="apple-touch-icon.png" />

The corners will still be automatically rounded, but the gloss effect will not be applied. Also, note that older Android versions (1.5 and 1.6) will only recognize the -precomposed tag.

Note

The text that's displayed on mobile devices' home screens, under your icon, will be whatever was placed in the <title></title> tags in your index.html file.

You can also specify different sizes of application icons for different device types:

<link rel="apple-touch-icon" href="apple-touch-icon.png" />

<link rel="apple-touch-icon" sizes="72x72" href="ipad-apple-touch-icon.png" />

<link rel="apple-touch-icon" sizes="114x114" href="iphone4-apple-touch-icon.png" />

This will allow you to customize the detail in the icon for different devices.

Apple iOS devices also allow you to specify a splash screen image that is displayed while your application is loading:

<link rel="apple-touch-startup-image" href="startup-image.png">

This image should be 320 x 460 px and in portrait orientation for iPhones. However, iPads can have different sized startup images, depending on whether they're in landscape or portrait orientation—748 x 1024 px for portrait and 1004 x 768 kpx for landscape.

You can specify different startup image sizes using media queries:

<link rel="apple-touch-startup-image" href="ipad-landscape-startup-image.png" media="screen and (min-device-width: 481px) and (max-device-width: 1024px) and (orientation:landscape)" />

<link rel="apple-touch-startup-image" href="ipad-portrait-startup-image.png" media="screen and (min-device-width: 481px) and (max-device-width: 1024px) and (orientation:portrait)" />

<link rel="apple-touch-startup-image" href="iphone-startup-image.png" media="screen and (max-device-width: 320px)" />

Media queries are a powerful tool for specifying configurations based not on the actual device but on its physical characteristics, such as screen size or pixel depth.

Note

If you'd like to learn more about media queries, a good place to start is this article: http://thinkvitamin.com/design/getting-started-and-gotchas-of-css-media-queries/.

Try it yourself

There's still plenty of room for improvement in our application, but we will leave this as extra credit for the reader. Some things you might want to try:

  • Adding paging, so that you can load more than the first page of 25 photos

  • Adding an expert search, where you can set your location manually or widen the search radius

  • Changing the theme and making the templates more appealing

  • Adding the ability to save locations as well as photos

Try using the MVC organization techniques we have covered in this chapter, to expand the application and sharpen your skills.

Summary

In this chapter, we gave you an introduction to the Model View Controller (MVC) design pattern. We talked about setting up a more robust folder structure and created your main application files. We started our application with an overview of the Flickr API and explored how to register our various model, view, and controller components. We then set up our components for the SearchPhotos and the SavedPhotos models. We wrapped up the chapter with some hints for putting the finishing touches on your application and talked about a few extra pieces you might want to add to the application.

In the next chapter, we will cover a few advanced topics like building your own API's, creating offline applications using a manifest system, and compiling applications with a program such as PhoneGap.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset