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 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:
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:
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:
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.
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.
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.
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.
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:
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.
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.
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.
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.
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.
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.
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 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:
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.
You should now see a list of photos near your location.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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/.
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.
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.