Sencha Inc. | HTML5 Apps

A Sencha Touch MVC application with PhoneGap

Published Jan 29, 2011 | James Pearce | Tutorial | Hard
Last Updated Oct 07, 2011

This Tutorial is most relevant to Sencha Touch, 1.x.

In this article, we introduce PhoneGap, a way to package and allow web applications to access device functionality. We use its APIs to create a custom data proxy which underpins a Sencha Touch MVC application. By the end of the article, we will have demonstrated how to replicate the iPhone's address book application using web technology alone.

Update, August 2011: this article (and the GitHub code) has been updated to reflect PhoneGap v1.0, Xcode v4.2, and Sencha Touch v1.1. It's been tested against iOS v4.3 and v5.

What is PhoneGap?

Sencha Touch is a powerful way to build mobile apps, and, because it relies entirely on web technologies, you can easily host those apps on your web server and have your users find and access them directly from their browsers.

Sometimes though, you'll want to distribute your application in different ways, including via various app stores. Imagine the best of both worlds: build an application with Sencha Touch and web technologies, and then distribute it alongside all of those other native apps.

Well, actually this need not take too much imagination, because it's very possible, and very easy - all through the magic of PhoneGap. This is an open-source deployment framework from Vancouver-based Nitobi (and contributed to by IBM, amongst others) which allows you to package your Sencha Touch code and resources into a binary app - ready to distribute to Apple, Android, Palm, Symbian, and BlackBerry devices.

However, it would do PhoneGap a great disservice to suggest that its only purpose is to distribute web apps. The framework's other purpose (and indeed the reason for its slightly unusual name) is to bridge the gap between the APIs that devices expose to native apps, and those which are made available to web apps (which are generally a significant subset, if they exist at all).

So, if you've ever wondered how to make your web app access a handset's file-system, use JavaScript to make it vibrate, prompt users to use the camera, or even check the device's current network state, then this hybrid approach is the answer.

As far as the JavaScript in a web app itself is concerned, PhoneGap simply provides a series of extra APIs through the global navigator object, specific to each of the areas of native functionality that are exposed. So, for example, navigator.camera.getPicture(..) allows your app to open the camera application to take an image (which is then available as a base64 string in your script), navigator.compass.getCurrentHeading(..) will return the device's orientation in degrees, and so on.

Naturally, these interfaces makes it possible to add a whole host of exciting functionality to Sencha Touch apps, and indeed make entirely new types of applications possible. PhoneGap gives us a glimpse of the web's future, without having to wait for browsers to implement these Device APIs natively.

To take PhoneGap for a bit of a spin, we've built our own address book application with Sencha Touch: it's an MVC-based app which overlays the address book database available through the navigator.service.contacts API. In this article, we explain how we did it.

Installing PhoneGap

The first thing we need to do is install PhoneGap for the platforms that we are targeting. A single download contains the SDKs required for each support operating system, and there are excellent installation instructions for each.

For the sake of this walkthrough, we are using PhoneGap v1.0. Also, we'll be using the iPhone SDK (and developing on a Mac). This article assumes you have Xcode v4.2 installed on your computer, although previous versions may also work. (If you have a more recent version of PhoneGap, you'll want to make sure you update the version numbers whenever the file is referenced, but API compatibility is generally preserved.)

In case the master PhoneGap download is not recent enough for you, you can download the latest iPhone SDK source files here and build the installer using the instructions in the README file. Once you have built the PhoneGapLibInstaller.pkg file and installed it, launch Xcode. We can now create a new PhoneGap application in the 'New Project' wizard. It looks something like this:

Let's name our new project 'Contacts' and provide a company-specific namespace for yourself. Then choose a location for your project file.

Finish the wizard and you'll drop straight into Xcode proper. The first thing you'll notice isthat the created project has the application's (native) structure ready to go, and some default project settings on the right:

In previous versions of Xcode, PhoneGap would create a www directory in the project, into which the web part of the application would be placed. In Xcode 4, this does not happen automatically, and you need to run the project at least once to get a www folder created. Do that now with the 'Run' button at the top of Xcode. All being well, the project will compile, the iPhone simulator will launch, and you'll have your first PhoneGap app running:

Not good. But if you go and inspect your directory structure with Finder, you will see that a neighboring directory has been created by the build process, complete with index.html and JavaScript files:

The recommended PhoneGap practice is to leave this directory here, and add it as a folder reference to the Xcode project. Right-mouse click the top of the project in the Xcode sidebar and 'Add Files to "Contacts"...'. You'll then be able to select the www directory that was created above. Preferably ensure that 'Create folder references for any added folders' option is selected, and that the 'Copy items into destination group's folder (if needed)' option is unchecked.

From now on, you'll see the web files at the top of the Xcode project. And when we run the app again, those example resources will now display:

(Note: if you ever have any issues with updates to the web part of the app not appearing in the build, you might find it useful to run Xcode's 'Product / Clean' sequence to refresh any changes.)

To build our full app, we now need to add to this www directory to make sure we're including the Sencha Touch resources, PhoneGap JavaScript library, and our own application logic. As per a standard Sencha application architecture, download the SDK and then place (or symlink) it within the lib directory, named touch. We will be putting our own code in the app directory, so your folder structure under www should look something like:

You can track the changes we make to the www directory in this tutorial's GitHub project. We'll be keeping branch checkpoints along the way.

IMPORTANT: When you are developing or experimenting, it's fine to simply place the whole SDK inside the lib folder like this. But when deploying, make sure that only the required JavaScript and CSS files remain in that folder, and remove the rest of the SDK. You do not want to compile and distribute the entire Sencha Touch SDK with your app.

If the app is to use PhoneGap's device APIs (as this one will), the PhoneGap JavaScript library must also be placed in the folder as a top-level file, and referenced in our virgin index.html file:

<!DOCTYPE html>
<html>
    <head>
        <title>Contacts</title>
        <script type="text/javascript" src="lib/touch/sencha-touch.js"></script>
        <link href="lib/touch/resources/css/sencha-touch.css" rel="stylesheet" type="text/css" />
        <script type="text/javascript" src="phonegap-1.0.0.js"></script>
        <script type="text/javascript">
            document.addEventListener("deviceready", function () {
	      alert('Our first PhoneGap app');
	    }, false);
        </script>
    </head><body></body>
</html>

(If you are using a different version of the PhoneGap JS file, you'll need to change the name phonegap-1.0.0.js, or symlink accordingly)

You might be surprised to see the use of addEventListener in this file, especially if you are used to using the Ext.setup convention for bootstrapping your application. The reason is that we need to register a listener for an event that fires when PhoneGap completes loading, called deviceready. Above, we're simply using it to demonstrate that the library has loaded and is ready. More on how this works in conjunction with Sencha Touch shortly...

Success - our first PhoneGap-powered application. This trivial starting point is the 1_basic_structure branch in the GitHub repo.

Of course it's possible to test and deploy your application onto a physical device too, but you will need to go through the process of creating certificates and provisioning files to do so. It's often a slightly fiddly process, but described in full in the iOS Reference Library. But this process has become much easier in Xcode 4, and if you're a native developer, it's likely you'll already have done this, of course.

There is one final step we need to take for this PhoneGap project's hygiene. These are documented in the PhoneGap install's readme.pdf, and involve setting the linkage for three of the iOS frameworks to being 'optional'. The frameworks of concern are CoreMedia, UIKit and AVFoundation, and this ensures compatibility with iOS 3.x devices. Find the linkage settings in 'Link Binary With Libraries' under 'Build Phases' in the Contacts target:

Now let's write some more code.

Instantiating the Application

Firstly let's create the folder structure for our MVC application. As per convention, we create an app directory, containing an app.js file, and then folders for controllers, models and views. Your complete folder structure should look (in Xcode) something like this:

In app/app.js, let's add a basic launcher for an Ext.Application instance, that will be namespaced as app:

Ext.regApplication({
    name: 'app',
    launch: function() {
        console.log('launch');
    }
});

Of course, don't forget to reference this new file in index.html:

<script type="text/javascript" src="app/app.js"></script>

You can also remove the alert() we used above. When you run this app, you should see the console log entry 'launch' appear in the Xcode console window. (Press Shift-Cmd-C to show this window in Xcode if it's not already open.)

However, before we do anything with this application, we want to be absolutely sure that the PhoneGap resources are loaded and that the APIs are available. This is where the deviceready event comes in: we want to be sure that that both the application launch and the deviceready event have fired before the application is bootstrapped. One way to do this is to check for the presence of the global device object, which is not available until the deviceready event fires.

Also, since it's not clear that we can be sure about the order in which the two events will fire, we should set a launched flag on the app object as well, which indicates that the app.launch() function has been run. Our real launcher function, which we can now call mainLaunch, will only execute when both conditions are met. We can call this from both the app.launch and deviceready handlers, now knowing that only the second attempt (when neither device nor app.launched are falsy) will proceed.

<script type="text/javascript">
    document.addEventListener("deviceready", app.mainLaunch, false);
</script>

And then in app.js:

Ext.regApplication({
    name: 'app',
    launch: function() {
        this.launched = true;
        this.mainLaunch();
    },
    mainLaunch: function() {
        if (!device || !this.launched) {return;}
        console.log('mainLaunch');
    }
});

A quick look at the logs will confirm that the 'mainLaunch' console message only appears once everything is loaded and ready.

Note that we've now eliminated any chance of the application launching on a desktop browser, since the device object will never be available in that context. If you are used to using Chrome or Safari as you develop and test Sencha Touch apps, you are out of luck when using PhoneGap. Depending on the PhoneGap APIs you choose to use, you might be able to try/catch them and spoof certain data for the purposes of running the app on a desktop browser (and lift the !device condition above when Ext.is.Desktop is true). But in reality, you should probably get used to using the Xcode simulator or a real device from here on.

As per application convention again, we will create a master Viewport view which will house the cards required for our address book (namely the list of contacts, a detail page, and a form to edit basic information about a contact).

Create views/Viewport.js, create a reference to it in index.html, and place in it the following code:

app.views.Viewport = Ext.extend(Ext.Panel, {
    fullscreen: true,
    layout: 'card',
    cardSwitchAnimation: 'slide',
    initComponent: function() {
        //put instances of cards into app.views namespace
        Ext.apply(app.views, {
            //contactsList: new app.views.ContactsList(),
            //contactDetail: new app.views.ContactDetail(),
            //contactForm: new app.views.ContactForm()
        });
        //put instances of cards into viewport
        Ext.apply(this, {
            items: [
                //app.views.contactsList,
                //app.views.contactDetail,
                //app.views.contactForm,
            ]
        });
        app.views.Viewport.superclass.initComponent.apply(this, arguments);
    }
});

Here, we are creating the app.views.Viewport component class, which extends a regular card-layout panel, and which will contain three cards. The initComponent method of the outer viewport will create an instance of each of the three inner card types and place those instances in the app.views namespace before placing them into its own items array.

The reason we instantiate these inner cards here (rather than in the global flow of code as you might see in some other examples) is that they will rely on data from the PhoneGap contacts API. We will instantiate the outer viewport in our application's mainLaunch event, which as we've just seen, will only execute when that API is ready. So by deferring the inner card instantiation too, we know that API will be ready. Also, by placing references to the card instances in the app.views namespace, we will be able to refer to them explicitly when we need to transition between them - rather than relying on their positional indices.

We've left the inner card code commented for now, as we haven't defined the list, detail, and form classes yet. But we can instantiate the outer viewport in the mainLaunch function in app.js:

mainLaunch: function() {
    if (!device || !this.launched) {return;}
    this.views.viewport = new this.views.Viewport();
}

Notice again how we are using the convention of a lower-case name for the single instance of the class (which starts with a capital letter). Both reside in the app.views namespace, which is created automatically by the Ext.regApplication function.

We can now close app.js: instantiating the outer viewport will trigger the creation of the rest of the UI. If you run the application in Xcode, you will now see the blank screen is a pale shade of gray - the subtle clue that Sencha Touch has created the fullscreen panel for us to build the UI onto. We've reached the 2_instantiation branch in the GitHub repo.

Modeling Data

An early step in any MVC-based application is to create the data models that will be used. Our UI component views need something to work with.

In our models folder, we can create the definition of the Contact model. This is the data structure which will contain basic information about a contact: their name, email addresses, phone numbers and so on.

Create models/Contact.js, create a reference to it in index.html, and then register our model in it:

app.models.Contact = Ext.regModel("app.models.Contact", {
    fields: [
        {name: "id", type: "int"},
        {name: "givenName", type: "string"},
        {name: "familyName", type: "string"},
        {name: "emails", type: "auto"},
        {name: "phoneNumbers", type: "auto"},
    ]
});

Perusing PhoneGap's contacts API documentation, we can see the fields that it makes available to us from the underlying device address book. For the sake of simplicity, we shall stick with first and last names, and the arrays of email and phone numbers attached to a contact. The fields supported via this API do vary slightly between OS platforms, so check the 'quirks' list in the documentation before using other fields, or on different runtimes. Also notice that again, we are using the (automatically generated) namespace of app.models to store a reference to the model.

We are being a little cheeky here with the email and phone number arrays. In a fully-fledged MVC app, we would probably want to represent PhoneGap's ContactField, ContactAddress classes and so on as Sencha Touch models too (especially if we wanted to be able to edit those lists) and then create associations between them and the contact that owns them. Here though, we just use an array of references to those raw PhoneGap objects (which is why we are using the 'auto' field type).

Collections of Sencha Touch models reside in stores, and we can create a simple store in this file too:

app.stores.contacts = new Ext.data.Store({
    model: "app.models.Contact"
});

This simply creates a store instance that can contain Contact models. Very simple. But now we need to figure out how to get data via the PhoneGap API into this store.

Models and stores in Sencha Touch are normally configured with a proxy. This is an object responsible for reading and writing model data to some other place, be it a local storage cache, or a remote server with AJAX. The framework provides a number of very useful proxy implementations itself, but of course it's possible to write our own. This provides quite an elegant way of getting the data in and out of the device's underlying address book database.

To create and register a custom proxy, named 'contactstorage', we simply extend the base Ext.data.Proxy class, at the start of the Contact.js file:

Ext.data.ProxyMgr.registerType("contactstorage",
    Ext.extend(Ext.data.Proxy, {
        create: function(operation, callback, scope) {
        },
        read: function(operation, callback, scope) {
        },
        update: function(operation, callback, scope) {
        },
        destroy: function(operation, callback, scope) {
        }
    })
);

Proxies provide CRUD functionality (create/read/update/destroy) for single records, or collections of records, and our class should implement the four corresponding functions above to do so. In actual fact, our application here will only implement read (to get the contacts from the address book) and update (to save simple changes back again). Of course, if you're feeling brave, it would be quite possible to implement the other two functions using PhoneGap's APIs for adding and deleting contacts too.

We should also make sure that the Contact model (and in turn its store) knows it needs to use this new type of proxy:

app.models.Contact = Ext.regModel("app.models.Contact", {
    fields: [...],
    proxy: {
        type: "contactstorage"
    }
});

Let's start with implementing the proxy's read function. This is passed an operation object which contains the details of the data that the store requires, and into which we will place the result set of contacts from the address book. This function is asynchronous, so once we're done, we'll also need to execute the provided callback.

The heart of this function needs to be our call to PhoneGap to retrieve the contacts. This uses the navigator.contacts.find method, which takes a list of the contact fields we want to pull out, and a function that is called on a successful fetch from the address book:

read: function(operation, callback, scope) {
    navigator.service.contacts.find(
        ['id', 'name', 'emails', 'phoneNumbers', 'addresses'],
        function(deviceContacts) {
           //success callback
        },
        function (e) { console.log('Error fetching contacts'); },
        {multiple: true}
    );
},

Note how we've only pulled out the fields that we want to place in our own simple Contact model. Also, this function takes two further arguments: an error callback to handle failure conditions and a set of options that can be used to search for specific records. We won't really use either in this simple walkthrough, but have set the minimum option required - to allow us to return multiple entries rather than just a single contact (which is the default).

(Note that in previous versions of PhoneGap, the navigator.contacts namespace was navigator.service.contacts. With PhoneGap 1.0, these APIs should now be stable.)

In the 'success' callback, we can now iterate through the device's contacts and create a collection of model instances for the proxy to return. The this scope will have changed in this callback, but we'd like to retain a reference to the proxy we're in (so we know the model type to instantiate and the scope to provide to the supplied callback). To do this, we can close our own callback over a thisProxy variable:

read: function(operation, callback, scope) {
    var thisProxy = this;
    navigator.contacts.find(
        ['id', 'name', 'emails', 'phoneNumbers', 'addresses'],
        function(deviceContacts) {
            //loop over deviceContacts and create Contact model instances
            var contacts = [];
            for (var i = 0; i < deviceContacts.length; i++) {
                var deviceContact = deviceContacts[ i ];
                var contact = new thisProxy.model({
                    id: deviceContact.id,
                    givenName: deviceContact.name.givenName,
                    familyName: deviceContact.name.familyName,
                    emails: deviceContact.emails,
                    phoneNumbers: deviceContact.phoneNumbers
                });
                contact.deviceContact = deviceContact;
                contacts.push(contact);
            }
            //return model instances in a result set
            operation.resultSet = new Ext.data.ResultSet({
                records: contacts,
                total  : contacts.length,
                loaded : true
            });
            //announce success
            operation.setSuccessful();
            operation.setCompleted();
            //finish with callback
            if (typeof callback == "function") {
                callback.call(scope || thisProxy, operation);
            }
        },
        function (e) { console.log('Error fetching contacts'); },
        {multiple: true}
    );
},

We're making sure that each model is being populated with the correct field from the PhoneGap deviceContact object, pushing it onto an array, and then placing the result set into the operation object. You might notice that we are also preserving a reference to the original deviceContact object in our model instance so that we can later update it and save back to the address book.

Talking of which, we may as well implement the corresponding update function of the proxy now:

update: function(operation, callback, scope) {
    operation.setStarted();
    //put model data back into deviceContact
    var deviceContact = operation.records[0].deviceContact;
    var contact = operation.records[0].data;
    deviceContact.name.givenName = contact.givenName;
    deviceContact.name.familyName = contact.familyName;
    //save back via PhoneGap
    var thisProxy = this;
    deviceContact.save(function() {
        //announce success
        operation.setCompleted();
        operation.setSuccessful();
        //finish with callback
        if (typeof callback == 'function') {
            callback.call(scope || thisProxy, operation);
        }
    });
},

Here again, we are passed an operation object (containing records to update) and a callback to fire when we are finished. Since our application will only be editing one contact at a time, we only need to implement this function to deal with operation.records[0]. (Also the application will only support updating of given and family names, so those are the only fields saved back to the address book.)

Firstly, we retrieve the original deviceContact reference from the model. Then we set its name object's givenName and familyName properties with the value that was in our Sencha Touch model. We save it back to the address book using the save method. This is also an asynchronous activity, so again we need to declare our update complete and successful in a callback, and in turn conclude with the callback we were originally provided.

Can we check this just worked? The first thing to do is make sure you have some real contacts data on the device. If you're running on a real device, that's possible, and some versions of the iOS simulators contain test data (which we'll be showing in this tutorial). If not, open up the regular iOS contacts app and add in a few test users.

Checking our data layer is also a good opportunity to introduce Weinre, a remote debugger for WebKit browsers. This is a very valuable piece of equipment if you're developing mobile apps on real devices or simulators - where there's no native debugging environment. Download, install, and run the (desktop-side) application from here, and, assuming you have the default port settings, add the following script to index.html to inject the (client-side) debugger to the application:

<script type="text/javascript" src="http://localhost:8080/target/target-script-min.js#anonymous"></script>

Once you run the Contacts app up again, you'll see the Weinre application indicate that the app has bound to it (in green):

Not only can you now navigate and inspect the DOM of the app running on the simulator (inside PhoneGap!), but we can use the console to load and query the datastore we defined above:

> app.stores.contacts
> app.stores.contacts.load()

This should allow you to inspect your phone's contacts, and to see that our model and store are configured correctly.

You can leave your Weinre debug harness in your app for now, although we won't be using it again in this article. You'll probably want to take it out before you put the app into production.

We're now done with our data layer and the PhoneGap integration, and have reached the 3_modeling_data branch in the GitHub repo. Let's now move onto our application's views and controllers.

Creating the Views

It makes sense to start with the contacts list view, as this also gives us another good way to check that our data layer is working. If you remember, we referred to the app.views.ContactsList component class in our outer viewport, so let's implement that. Add the views/ContactsList.js file, and refer to it in your index.html, and create that component by extending an Ext.Panel to contain a toolbar and list:

app.views.ContactsList = Ext.extend(Ext.Panel, {
    dockedItems: [{
        xtype: 'toolbar',
        title: 'Contacts'
    }],
    layout: 'fit',
    items: [{
        xtype: 'list',
        store: app.stores.contacts,
        itemTpl: '{givenName} {familyName}',
        onItemDisclosure: function (record) {
            //Ext.dispatch({
            //    controller: app.controllers.contacts,
            //    action: 'show',
            //    id: record.getId()
            //});
        }
    }],
    initComponent: function() {
        app.stores.contacts.load();
        app.views.ContactsList.superclass.initComponent.apply(this, arguments);
    }
});

Unfortunately, in Sencha Touch 1.x, it's not possible to dock a toolbar directly onto an Ext.List, hence the panel wrapper.

Our list is bound to the store of Contact instances we created in the previous section, and we use a small template to indicate that we want the given and family names to show for each contact. We will use Ext.List's disclosure feature to place an arrow on each item that will slide to the detail page for it. Since that detail component and the controller that will drive the transition to it aren't yet written, we'll comment the dispatcher out. But the presence of the onItemDisclosure property will at least display the arrow for us for now.

Also, we need to explicitly force the store to load its data in this control's instantiation. We didn't use the popular autoLoad property on the store (since that might have fired the loading prior to the deviceready event), so we need to ensure the store is loaded before the list displays.

Going back to Viewport.js, we can now uncomment the instantiation of this component as contactsList and make sure it is placed in the viewport's card sequence:

app.views.Viewport = Ext.extend(Ext.Panel, {
    fullscreen: true,
    layout: 'card',
    cardSwitchAnimation: 'slide',
    initComponent: function() {
        //put instances of cards into app.views namespace
        Ext.apply(app.views, {
            contactsList: new app.views.ContactsList()
            //contactDetail: new app.views.ContactDetail(),
            //contactForm: new app.views.ContactForm()
        });
        //put instances of cards into viewport
        Ext.apply(this, {
            items: [
                app.views.contactsList
                //app.views.contactDetail,
                //app.views.contactForm,
            ]
        });
        app.views.Viewport.superclass.initComponent.apply(this, arguments);
    }
});

With our fingers crossed, we should now be able to launch the application and see the list containing our fetched records. Depending on the contact records present on your device you should see something like this:

Next, we should create the detail page for each contact record. This will be an instance of the ContactDetail component class, which we can create in a new file called views/ContactDetail.js (also referenced in index.html of course). The component has a docked toolbar at the top, which will contain the contact's name and two buttons - one to go back to the list, and one to move onto the editing form.

app.views.ContactDetail = Ext.extend(Ext.Panel, {
    dockedItems: [{
        xtype: 'toolbar',
        title: 'View contact',
        items: [
            {
                text: 'Back',
                ui: 'back',
                listeners: {
                    'tap': function () {
                        //Ext.dispatch({
                        //    controller: app.controllers.contacts,
                        //    action: 'index',
                        //    animation: {type:'slide', direction:'right'}
                        //});
                    }
                }
            },
            {xtype:'spacer'},
            {
                id: 'edit',
                text: 'Edit',
                ui: 'action',
                listeners: {
                    'tap': function () {
                        //Ext.dispatch({
                        //    controller: app.controllers.contacts,
                        //    action: 'edit',
                        //    id: this.record.getId()
                        //});
                    }
                }
            }
        ]
    }],
    styleHtmlContent:true,
    scroll: 'vertical',
    items: []
});

Again, we've commented out the actual event logic for now, since we haven't created the relevant controllers yet. But now at least we can return to the viewport and uncomment the instantiation of, and reference to, the ContactDetail component instance. We've reached the 4_views branch in the GitHub repo.

The Controller

We're now at a point where we need to start wiring up the view components. Although we have the contact detail page, it's blank, and there's no way to get to it. We need the app to react when the user clicks the disclosure arrow on the list, for example. We could put this logic directly into the disclosure event handler, but let's fulfill our MVC ambition by doing this in a discrete controller. We only need one controller, but it will contain a few different actions, referring to verbs that we want to apply to contacts.

Create a controllers/contacts.js file and add it to index.html. We don't need to create a class here, and can just directly instantiate an Ext.Controller into the app.controllers namespace:

app.controllers.contacts = new Ext.Controller({
    index: function(options) {
    },
    show: function(options) {
    },
    edit: function(options) {
    }
});

We want to be able to index (i.e. list) contacts, or show or edit a single one, so we'll need the three actions above.

(It's not useful here, since our PhoneGap app won't be running in a browser with an address bar, but in a regular web app, we would be able to bind URL fragments to these controller/action pairs. This allows users to deep link into different states of the application through the use of 'routes', and would allow us to create in-app history support.)

Let us implement the show action first:

show: function(options) {
    var id = parseInt(options.id),
        contact = app.stores.contacts.getById(id);
    if (contact) {
        app.views.contactDetail.updateWithRecord(contact);
        app.views.viewport.setActiveItem(
            app.views.contactDetail, options.animation
        );
    }
},

This assumes that it has been passed an options object containing the id of the contact to be displayed, and attempts to fetch the record from the store. If found, it updates the detail card with the relevant contact record, and then slides the card into focus as the viewport's active item.

Before this works, let's quickly implement that updateWithRecord method on the ContactDetail panel:

updateWithRecord: function(record) {
    var toolbar = this.getDockedItems()[0];
    toolbar.setTitle(record.get('givenName') + ' ' + record.get('familyName'));
    toolbar.getComponent('edit').record = record;
}

This simply gets a reference to the toolbar, sets its title to the contact's name, and also puts a reference to the record on the 'Edit' button (so that it, in turn, will know the record that needs to be bound to the edit form we haven't written yet).

Finally, uncomment the dispatcher in the disclosure function of the ContactsList panel:

Ext.dispatch({
    controller: app.controllers.contacts,
    action: 'show',
    id: record.getId()
});

This executes the controller's show action that we just created, ensuring that the relevant contact's id is passed through in the options object, as required.

Stop and recompile your application in Xcode, and you should now find that clicking the disclosure button will transition the viewport to the second card:

What's that? The back button doesn't work? Of course we need to wire up a controller action to that too.

Implementing the index action in the controller is simple. It needs only to make sure the viewport is set to show the ContactsList view:

index: function(options) {
    app.views.viewport.setActiveItem(
        app.views.contactsList, options.animation
    );
},

And we also need to make sure the back button in the ContactDetail panel dispatches the call to this controller. Unlike the disclosure button, it doesn't need to pass in the id of a contact (since it is simply returning to a list of all the contacts), but it would also be nice if the slide was reversed to give the impression of returning to the list:

{
    text: 'Back',
    ui: 'back',
    listeners: {
        'tap': function () {
            Ext.dispatch({
                controller: app.controllers.contacts,
                action: 'index',
                animation: {type:'slide', direction:'right'}
            });
        }
    }
},

You can test that the interactions work as expected. (Note that the 'Edit' button still won't do anything.)

Our detail page is currently blank, so let's put some information on the page. We can do this by placing sub-sections on the ContactDetail panel, each of which has a template to iterate through multiple contact fields. (Remember, the email and phone number fields are arrays of multiple PhoneGap ContactField objects, each with a type and value.)

items: [
    {tpl:[
        '<h4>Phone</h4>',
        '<tpl for="phoneNumbers">',
            '<div class="field"><span class="label">{type}: </span><a href="tel:{value}">{value}</a></div>',
        '</tpl>'
    ]},
    {tpl:[
        '<h4>Email</h4>',
        '<tpl for="emails">',
            '<div class="field"><span class="label">{type}: </span><a href="mailto:{value}">{value}</a></div>',
        '</tpl>'
    ]},
]

Notice that we use the tel: URI scheme in the phone numbers' links to make them click-to-call, and mailto: on email addresses so they launch the device's mail application.

Then, to make sure that these sub-panels get updated to be bound to the same record as the ContactDetail as a whole, let's add a small loop to the start of updateWithRecord method to do so. Sencha Touch doesn't automatically recurse data updates through child components.

updateWithRecord: function(record) {
    Ext.each(this.items.items, function(item) {
        item.update(record.data);
    });
    var toolbar = this.getDockedItems()[0];
    toolbar.setTitle(record.get('givenName') + ' ' + record.get('familyName'));
    toolbar.getComponent('edit').record = record;
}

That is sufficient to get us the famous John Appleseed's telephone number:

Now, I'm no designer, but this requires a little bit of styling. We could of course us Sass to completely style this application, but for now, let's (slightly crudely) add some styling to the <head> of index.html to get these fields nicely arranged:

<style>
    h4 {
        background: #F7F7F7;
        font-weight: bold!important;
        margin: 0.5em 0 0!important;
        padding: 5px;
        border: solid #DDD;
        border-width: 1px 0;
    }
    .field {
        background:#FFFFFF;
        font-size: 120%;
        text-indent: -100px;
        border-bottom: 1px solid #DDD;
        padding:0.4em 0.4em 0.4em 100px;
    }
    .field .label {
        display: inline-block;
        color:#666;
        text-align:right;
        text-transform:capitalize;
        width: 100px;
        padding-right:1em;
    }
</style>

To make the most of the fact that we've built this application with web technology, it might be fun to add a few more templates. Perhaps link to Google or Bing to search for the contact? Another sub-panel will take care of that:

{xtype:'panel', tpl:[
    '<h4>Search</h4>',
    '<div class="field"><span class="label">Google: </span><a href="http://www.google.com/m/search?q=%22{givenName}+{familyName}%22">\'{givenName} {familyName}\'</a></div>',
    '<div class="field"><span class="label">Bing: </span><a href="http://m.bing.com/search?q=%22{givenName}+{familyName}%22">\'{givenName} {familyName}\'</a></div>',
]}

All of which then gives us:

We've reached the 5_controller branch in the GitHub repo.

Note that there is a small problem with linking to those external web sites in PhoneGap: by default this external page will launch 'in' your application, which, because there's no back button, means your users are stuck in Google or Bing and can't return to the contacts application without restarting it. PhoneGap ships this way to discourage people from creating very thin apps that pull their content in from external servers (and which won't generally get accepted to Apple App Store as a result). But it's possible to alter that behavior for apps - such as this one - which do occasionally need to shell out to external sites.

To do this, and if you're happy editing the Objective-C part of your PhoneGap application, find the AppDelegate.m file in the 'Classes' part of the project, and find webView:shouldStartLoadWithRequest:navigationType (which will be around line 96 or so). We need to override this function to detect whether the URL's scheme is HTTP or HTTPS. If so, it should launch Safari, and if not (i.e. a file:// URL from the resources of the PhoneGap app itself) it should use the app's container. The code below will suffice:

- (BOOL)webView:(UIWebView *)theWebView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
    NSURL *url = [request URL];
    if ([[url scheme] isEqualToString:@"http"] || [[url scheme] isEqualToString:@"https"]) {
        [[UIApplication sharedApplication] openURL:url];
        return NO;
    }
    else {
        return [ super webView:theWebView shouldStartLoadWithRequest:request navigationType:navigationType ];
    }
}

This now launches a separate window and allows users to multi-task back to the Contacts app.

Editing Contacts

The final card of our application is a form that will allow users to edit a few of the contact fields. This needs to be an instance of the ContactForm class. Create the views/ContactForm.js file and a reference to it into index.html. The component extends Ext.form.FormPanel and features a toolbar, two fields, and, like the detail page, an updateWithRecord function that a controller can use to bind data to it:

app.views.ContactForm = Ext.extend(Ext.form.FormPanel, {
    dockedItems: [{
        xtype: 'toolbar',
        title: 'Edit contact',
        items: []
    }],
    submitOnAction: false,
    items: [{
        name : 'givenName',
        label: 'Given',
        xtype: 'textfield'
    }, {
        name : 'familyName',
        label: 'Family',
        xtype: 'textfield'
    }],
    updateWithRecord: function(record) {
        this.load(record);
    }
});

This card is reached by pressing the 'Edit' button on the ContactDetail page. Let's add a suitable dispatch call to that button:

{
    id: 'edit',
    text: 'Edit',
    ui: 'action',
    listeners: {
        'tap': function () {
            Ext.dispatch({
                controller: app.controllers.contacts,
                action: 'edit',
                id: this.record.getId()
            });
        }
    }
}

...and implement the edit action of the controller, which is exactly the same as that for show, except that this time it binds the record to, and slides in, the ContactForm panel:

edit: function(options) {
    var id = parseInt(options.id),
        contact = app.stores.contacts.getById(id);
    if (contact) {
        app.views.contactForm.updateWithRecord(contact);
        app.views.viewport.setActiveItem(
            app.views.contactForm, options.animation
        );
    }
}

Don't forget to uncomment the instantiation of this third card in the outer viewport:

app.views.Viewport = Ext.extend(Ext.Panel, {
    fullscreen: true,
    layout: 'card',
    cardSwitchAnimation: 'slide',
    initComponent: function() {
        //put instances of cards into app.views namespace
        Ext.apply(app.views, {
            contactsList: new app.views.ContactsList(),
            contactDetail: new app.views.ContactDetail(),
            contactForm: new app.views.ContactForm()
        });
        //put instances of cards into viewport
        Ext.apply(this, {
            items: [
                app.views.contactsList,
                app.views.contactDetail,
                app.views.contactForm
            ]
        });
        app.views.Viewport.superclass.initComponent.apply(this, arguments);
    }
});

We can now click all the way through to the form containing the two fields in question:

This form is currently a dead-end though. We need to implement both a 'Cancel' button (that takes us back to the detail page) and an 'Apply' button (that saves the record but also returns to the detail). Let's add those two buttons to our form's toolbar:

items: [
    {
        id: 'cancel',
        text: 'Cancel',
        ui: 'back',
        listeners: {
            'tap': function () {
                Ext.dispatch({
                    controller: app.controllers.contacts,
                    action: 'show',
                    id: this.record.getId(),
                    animation: {type:'slide', direction:'right'}
                });
            }
        }
    },
    {xtype:'spacer'},
    {
        id: 'apply',
        text: 'Apply',
        ui: 'action',
        listeners: {
            'tap': function () {
                this.form.updateRecord(this.record, true);
                this.record.save();
                Ext.dispatch({
                    controller: app.controllers.contacts,
                    action: 'show',
                    id: this.record.getId(),
                    animation: {type:'slide', direction:'right'}
                });
            }
        }
    }
]

The first item resembles a back button and simply dispatches the show action of the controller as before but with a right slide (to give the appearance of returning). The right-hand button is exactly the same, but updates the bound record with the form's data, and saves the record, before sliding the view back.

The record.save() method will fire the update method of our proxy to persist the change back to the underlying phone address book, so the effect will be asynchronous, but essentially immediate.

Finally, we need to make sure that the two buttons are given a reference to the edited record when the form first slides into view, as well as the form itself so that the 'Apply' button can reference the data on it:

updateWithRecord: function(record) {
    this.load(record);
    var toolbar = this.getDockedItems()[0];
    toolbar.getComponent('cancel').record = record;
    toolbar.getComponent('apply').record = record;
    toolbar.getComponent('apply').form = this;
}

The form now looks like this:

...and works! Make a small change here, and you'll see that not only is our own app immediately updated with the change (since both list and detail cards are bound to the model instance in the store), but the phone's native address book has also been updated:

Mission accomplished. We've reached the 6_editing branch in the GitHub repo.

Finishing Touches

As a final flourish, let's make a few small cosmetic adjustments to our application. Firstly, we can make the initial list look a little more like the native address book application by grouping the entries under their family name's first initial and adding an index bar to the right-hand side. Add:

grouped: true,
indexBar: true,

to the configuration of the list inside ContactsList, and add a getGroupedString function to the suitably-sorted store:

app.stores.contacts = new Ext.data.Store({
    model: "app.models.Contact",
    sorters: 'familyName',
    getGroupString : function(record) {
        return record.get('familyName')[0];
    }
});

The result is effective (especially if you get to run this on a handset or simulator with more than 6 contacts on it):

Also, you might have noticed that PhoneGap is creating the application with a default icon and splash screen. We can update these in the Xcode project, by editing (or replacing) the relevant files in the project settings. The icon should ideally be provided in three sizes: 57px, 72px and 114px (for different iOS form factors), and named icon.png, icon-72.png and icon@2x.png respectively.

The splash screen files need to be 320x480px, 768x1024px and 1024x768px and are called Default.png, Default-Portrait.png and Default-Landscape.png respectively. We've put some sample ones in the assets folder of the master, and final, branch of the GitHub repo (which we've now reached!).

Who knows? Perhaps you can replace your native address book...

Summary

Well, that's it - we've finished a fairly rigorous walkthrough of a Sencha Touch contacts application.

We started off by installing PhoneGap for iOS, creating a project, and invoking our Sencha Touch application when the PhoneGap API is ready.

We created an MVC application architecture, and used a model layer that used a custom proxy to retrieve and store simple contact instances via the PhoneGap contacts API.

Then we developed three different view classes for the three sliding panels of the application: the contacts list, a detail page, and the editing form. These were wired together using controller actions which ensured that references to contact records were correctly bound to the templates and form fields.

Finally we showed that it all works, and that changes to the records in our application are propagated down through PhoneGap to the underlying address book. Add a shiny icon and a splash screen and we're done.

Of course there are plenty of ways to take this further. For a start, it would be useful to be able to see more detail than just email and phone numbers, and be able to edit more than just the names. This would require nothing fundamentally different - just making sure that more fields were extracted from PhoneGap onto the model, bound to the field, and then written back again. Also the app could use a 'refresh' feature to pick up changes made via the native app after this app has already loaded.

It would also be a good exercise to demonstrate that the same web app executes on other platforms. Although PhoneGap's contacts API is one of the most recently-changed parts of the framework, it should work in theory in both Android and BlackBerry WebWorks environments.

And as an exercise for the reader, why not try hooking up extra PhoneGap APIs? A little geolocation and you could highlight contacts who live in your current area.

And finally, if you build an application that's sufficiently different to the native address book, why not consider publishing it? After all, that's where we started at the beginning of this article: your Sencha Touch web app is now a compilable, deployable binary distributable, ready to hit the app stores. Have fun!

Share this post:
Leave a reply

Written by James Pearce
James Pearce heads developer relations at Sencha. He is a technologist, writer, developer and practitioner, who has been working with the mobile web for over a decade. Previously he was the CTO at dotMobi and has a background in mobile startups, telecoms infrastructure and management consultancy. James is the creator of tinySrc, the WordPress Mobile Pack, WhitherApps, modernizr-server and confess.js, and has written books on mobile web development for both Wiley and Wrox.
Follow James on Twitter

60 Comments

Tobias Reike

3 years ago

Thanks for this deep look into real Sencha+Phonegap development.
But I have a problem running this Sencha app on Android devices, the contacts list view is not scrollable, even though I see that there are more items in the list. When trying to scroll down the listview just skips back to the beginning as if there are no more items available.

Would be great if I could somehow fix this. I really would like to use Sencha+Phonegap for a future project, but it has to work on iPhone AND Android devices.

Thanks a lot for your help!

James Pearce Sencha Employee

3 years ago

Hi Tobias, we’re planning on an update to this article due to changes in the APIs and Xcode itself.

To address the scrolling issue (for longer lists), mea culpa. Add layout:‘fit’ to the config for the panel and it will constrain the viewport to the screen so the scrolling works. This should also appear in the updated article… stay tuned

Hartti

3 years ago

There are a couple of typos in the examples on this page. Those typos do not exist in the git version of the app.

For example the row
itemTpl: ’ ‘,
should be
itemTpl: ‘{givenName} {familyName}’,

and the row
var deviceContact = deviceContacts\<i\>;
should be
var deviceContact = deviceContacts\[i\];

Terry

3 years ago

Has anyone mentioned the error “DataView requires tpl, store and itemSelector configurations to be defined.”?  I’ve seen several sites with postings about this error message with no solution.  I’ve tried many different things but keep getting this error.  I am using XCode4 with the latest version of PhoneGap.

shuh

3 years ago

Hi,

I tried to test the application on Chrome, but nothing appearing.
the error: app.js:Uncaught ReferenceError: device is not defined.

what can u suggest ? i did follow the readme file but still no luck.

thanks

Jörg Spieler

3 years ago

Is there anywhere a downloadable sencha touch phonegap demo working with Xcode 4? Can be simple.

James Pearce Sencha Employee

3 years ago

Hi Jörg, yes, we’re updating this article, not only for Xcode 4, but more importantly for the updated PhoneGap 1.0 API. Stay tuned!

Chinmay patel

3 years ago

Hey, I have downloaded the code and added the Sencha Library, in the correct folder. However, my screen is blank, every time I run it. I ran into the same issue, while I was following the tutorial line by line. However, everything stopped working at code.

“Ext.regApplication({
  name: ‘app’,
  launch: function() {
      console.log(‘launch’);
  }
});”

I guess, that does not work for me. Please keep in mind that I am using Eclipse with Google Nexus One device, in place of Xcode with iPhone emulator.

Does anyone have any idea, why that could happen?

Jörg Spieler

3 years ago

Hello again.

Now i can wait for an update because meanwhile I did my own experiences following the screencasts. With my knowledge in phonegap I have made a tiny sencha-phonegap-app and it works fantastic.

Background: Over the last months I am working for a customer on an IOS-app with Jquery Mobile with dynamic content. JQM is so slow that I have now an angry customer and a (un-)finished project. Maybe I release that with JQM and build it new with sencha touch.

 

James Pearce Sencha Employee

3 years ago

Jörg, I’m glad to hear that you’re getting on well. We’ll update the tutorial shortly, and in the meantime, I’m glad to hear your success story! Please let us know how your project goes.

Jörg Spieler

3 years ago

a little bit off topic: the difference between JQM and sencha touch is that jqm is very easy at the beginning. With a bit of knowledge in html5 and jquery you can make very easy Web(!)-Apps. When you want dynamic native-apps the results are not perfect. Sencha touch (I thought) is not so easy at the beginning but if you know something bout programming it is a very powerful tool for working. That’s what i think now wink

Marius S

3 years ago

HI guys,
yesterday i started to develop witch sencha touch. I tried this tutorial but i get some Errors:

07-28 14:34:19.051: ERROR/Web Console(4117): Uncaught ReferenceError: Ext is not defined at file:///android_asset/www/app/app.js:1

07-28 14:36:27.311: ERROR/Web Console(4117): Uncaught ReferenceError: app is not defined at file:///android_asset/www/app/views/Viewport.js:1

somebody can help me?

Keith Levi

3 years ago

I would love to see the updated article for Android (and also Blackberry).

Is that coming soon? 

Handy Wang

3 years ago

Some wrong writing:
      i) In file app/models/Contact.js
      var deviceContact = deviceContacts; should be var deviceContact = deviceContacts[ i ];
    ii) In file app/views/ContactsList.js
      itemTpl: ’ ‘, should be itemTpl: ‘{givenName} {familyName}’,

Handy Wang

3 years ago

Hi James,
Firstly, Thanks for this fantastic article.
However, I encounter a problem, that is when I deploy this demo in my device iPodTouch4, while launching the splash view is Phonegap logo, disappear , then flash a little and then ‘Contacts’ appear. So, I confused that why did the screen content flash after splash view disappear and before Contacts list appear?

I guessed the point is “Ext.regApplication” in file app.js ?

If you get the answer, pls let me know. Thanks a lot.

James Pearce Sencha Employee

3 years ago

@Hartti & @Handy yes, we had a zealous CMS template that was swallowing some magic syntax. It should be back and clear now in the main article.

Andres Rodriguez

3 years ago

Hi, if you are using phonegap 1.0 the correct line 10 of contact.js:

s/navigator.service.contacts/navigator.contacts

Handy Wang

3 years ago

@ Andres Rodriguez
Hi buddy, what did you mean?

Andres Rodriguez

3 years ago

If you try to compile under phonegap 1.0, it won’t work, because the file models/Contact.js is referring to an incorrect path for the device contacts:

navigator.service.contacts

It should be:

navigator.contacts

This is line 10 of the Contact model.

Handy Wang

3 years ago

@ Andres Rodriguez

OK, Got it!

James Pearce Sencha Employee

3 years ago

Hi everyone,

I’ve updated this article (and the GitHub code) to reflect PhoneGap v1.0, Xcode v4.2, and Sencha Touch v1.1. It’s been tested against iOS v4.3 and v5, and should solve some of the issues you were experiencing with out of date APIs etc.

Enjoy

Ash

3 years ago

I tried the same app on my machine. It runs fine as-is.
I tried to make a change by reading it from a hard-coded data set using the same Proxy “contactstorage” in models/Contact.js. The data is defined in the store (app.stores.contacts) as—
data: [
  {“id”:1,“givenName”:“Ashish”,“familyName”:“Mehrotra”,“emails”:“bcd@yahoo.com”,“phoneNumbers”:“1 (234) 567-8900”},
  {“id”:2,“givenName”:“Paypal”,“familyName”:“Kapoora”,“emails”:“abc@yahoo.com”,“phoneNumbers”:“1 (234) 567-8999”}
]
In the read method of the proxy, instead of making a call to navigator.service.contacts.find(),
I do—
deviceContacts = app.stores.contacts.data;

But I am not able to read the data. Why is that no happening?

shuh

3 years ago

i tried Sencha Touch + Phonegap on Andorid 2.2 and it seemed quite slow in terms of page rendering and navigation. Is it normal expected result or there is way to optimize it for speed ? Comparing to Native App events, Sencha Touch events quite late in response, while scrolling the list mostly it fires the the click event. Has anyone experienced this ?

thanks.

Qiang

3 years ago

Thanks for the great tutorial.
I tried the example on the simulator, and found the screen flicks right after the PhoneGap splash screen disappears, which was quite annoying. Is there any fix to that?

Thank you!

Handy Wang

3 years ago

In my situation, the screen will flash after PhoneGap splash view disappears, and the my main app is launching.

I asked this question for about one week , but there is no body to response.

Dave R

3 years ago

Good article, James, helpful for understanding PhoneGap and Sencha.

I worked through it in Android from Eclipse on a Win7 64 box; wasn’t able to get Weinre working for the emulator; also the contact names are coming back on the emulator (when I enter contacts into the built in contacts) but when I run it on a device it only pulls up emails (which I added).  Odd.  I’d thought it might be because first name / last name aren’t broken out, so I switched to the formatted name but that didn’t work either.

Jo Seemoore

3 years ago

This is how I setup my application without having to create two weird functions and a script on the html side (also allows me to have private var and such in my App class instance)

var App = function()
{
this.name = “app”;

// Sencha App launch
this.launch = function()
{
  // Phonegap Device not ready?
    if (!device) { //  && !Ext.is.Desktop
  document.addEventListener(“deviceready”, app.launch, false);
  return;
  }
 
  this.views.viewport = new this.views.Viewport();
 
  console.log(‘launched’);
}
}

var app = Ext.regApplication( new App() );

Michael

3 years ago

Could you explain the advantage of using Ext.extend if most of these components are one-off and not reused?  Wouldn’t the code be simpler and more understandable if we create instances of say Ext.Panel directly, without first having to extend it and keep track of an additional class name?

ManuB

3 years ago

Very good article, what a great work.
I followed every steps carefully, but i was not able to make it work. I even took the last master branch, and tried it, and i have a “loading” screen. I checked, i have some contacts in my iPhone simulator.
Am i the only one who have this issue ?

James Pearce Sencha Employee

3 years ago

@ManuB a blank screen normally implies a JavaScript error, and this could easily be that the Sencha Touch JavaScript library is not being referenced properly, or something similar.

Could I suggest you use Weinre to connect a remote debugger to the app? It’s described in the middle part of the tutorial. You should immediately see the exception being thrown.

Even more easily, set the developer debugging on in the iOS Safari settings. It’s not a rich debugger by any means! But it does at least show you the major issues occurring in a toolbar in the mobile browser.

James Pearce Sencha Employee

3 years ago

@Michael, agree. I guess it’s habit. But also in this case, I wanted to be sure that the PhoneGap APIs were ready before I tried to instantiate components bound to their data… this pattern helped remind me that the instantiation needed to happen in that deferred mainLaunch function, while still being able to keep the class code off in separate files.

James Pearce Sencha Employee

3 years ago

@Handy, I would suggest the same thing as @ManuB: either turn on the Safari debugger, or add in the Weinre script at the start of your app so you can remote-debug. At least then see what the main exception is.

James Pearce Sencha Employee

3 years ago

@Ash, for loading data from a hard-coded object declaration, I would recommend trying the ‘data’ configuration of Ext.data.Store directly (and then a MemoryProxy if you need to update it). See the ‘Inline data’ section of http://dev.sencha.com/deploy/touch/docs/?class=Ext.data.Store and info on memory proxies here: http://dev.sencha.com/deploy/touch/docs/?class=Ext.data.MemoryProxy

manuB

3 years ago

Thx James, for your so quick answer.
I did use weinre, to detect the problem. And i have a js error,
here is this error : http://files.emmanuelbernard.me/error.png

I took the last sources from the master, and final, branch. To figure how if i had a problem somewhere else smile

I don’t understand what i did wrong.

James Pearce Sencha Employee

3 years ago

@ManuB, my guess is that you maybe have a contact in your address book that doesn’t have a name field (or something like that)... this exception is being raised at the PhoneGap level I think. Did you create artificial contacts or are they real ones?

ManuB

3 years ago

I did generate some fictiv contact, i had a contact who has just a “compagny name” and no user name, that was the bug you are right smile
thx

Dave R

3 years ago

Update on my earlier post - turns out that displayName is a more reliable property for getting the contact name on my device so I switched over to that.

Weinre still not working for me.

Ganz

3 years ago

hi…
This example is iOS user…..
If you have Android example….. would you send example from my email?

James Pearce Sencha Employee

3 years ago

@DaveR - Weinre is reliable for me, but you might have a different port configuration I guess. Where and how does it fail? Make sure the script snippet is the same as the Weinre app shows you’ll require in your HTML.

James Pearce Sencha Employee

3 years ago

@Ganz, there seems to be enough interest in an Android example for us to create another tutorial at some point! That said, there’s nothing - in theory - in this app that wouldn’t work on Android as-is. PhoneGap’s contacts API has a few quirks on different platforms, but easy to work with I think.

James Pearce Sencha Employee

3 years ago

@iPhone - that doesn’t seem strictly like a Sencha Touch question, so it will depend on your app’s behaviour. You’ll need to configure the app to work in multiple orientations - can you see the options at the bottom of http://img3.sencha.com/files/learn/p11b.png ? Try those and see if it helps…

Amit

3 years ago

Aha…..Great tutorial

Denis

3 years ago

Hey guys
Was able to Complete this tutorial and it’s great.
The only issue that u’re missing placeholders like this: {givenName} {familyName} in your tpl examples.
All other things worked just good for me.

my environment: phonegap 1.0.0, sencha, xCode 4

Roland

3 years ago

Does not work on iOS 5 beta 5 on the actual device (iPhone 4).
Works in the simulator only :-(

Joe Kennedy

3 years ago

has anyone been able to get this to run on Android.  I downloaded the code and like Chinmay above all I get is a white screen.  I really need to get this to run on Android can anyone assist?

leela

3 years ago

hi,
could any one pls tell me how to include images using sencha touch?
and
how to retrive text field entries?
say suppose in login.js i enter login name and password.
How to store these values in model/store and how to access back in any one of views/————.js file?

Joe Kueser

3 years ago

I tried to add a button to the contact details view, but just putting “{xtype: ‘button’, text: ‘I am a button’}” in the top of the items list. When I first view the details view, the button shows up just as expected, with the obvious “I am a button” text. However, if I go back to the list, and then click on another contact (or the same contact for that matter) the button is blank.

I’ve gone so far as trying to set the button’s text in the updateWithRecord method, which, again, works like a charm the first time, but results in an empty button every subsequent time.

Any ideas on why this would be happening? Or more important, how to make it so it doesn’t happen?

Thanks!

John Doe

3 years ago

Hey, nice post, could you post one similar for PhoneGap + Eclipse + Android.
Apparently there is no beginners guide to a simple MVC app with the above tools.
Thanks.

Andy Roberrts

3 years ago

Hey could you please do a demo for sencha touch+android+eclipse+phonegap.
Apparently there is no MVC sample tutorial to start with.

NutTao

3 years ago

I cannot add
grouped: true,
indexBar: true

simulator can’t load list page (still in Loading…)
console : Error in success callback: com.phonegap.contacts.search0 = TypeError: Result of expression ‘a.name’ [undefined] is not an object.

what should I do?

Tarun Sharma

3 years ago

Hi This is a great article thanks for this.
As i was playing with this code and actually used a phonegap plugin for native controls. I used the very first view port and in it the “PGContacts.views.ContactsList” panel. When i use this plugin to show a UITabBar at the bottom (basically this tab bar is added in the UIWebView’s viewcontroller’s view) and the webview is shifted upward by the tab bar height.
Everything works very well but now a strange thing happens. As you tap on the panel/toolbar the whole toolbar goes upward behind the status bar. At once i thought this could be due to scrolling on panel is true so i did it false but no good.
Can anyone tell me why this strange behavior? Is there some css class which is making this happen or is this a property of the web view??

eilijah

3 years ago

Any documentation about ProxyMgr and/or navigator? I can’t find anything in the api ref.

Yi

3 years ago

Hey, I just tried this tutorial, using Xcode 4.1 and phonegap 1.0. I can get the tutorial working until the deviceready part, after that when I added the app.js with the following script, the console output is not showing the expected log. Any ideas?


“Ext.regApplication({
  name: ‘app’,
  launch: function() {
    console.log(‘launch’);
  }
});”

Mike Miler

3 years ago

Great tutorial!  It translated almost one-to-one for Android & Eclipse.  The only gotcha I encountered was in connecting to weinre.  Running the emulator, the address of the server is 10.0.2.2, not ‘localhost’

ben wu

3 years ago

when I tested codes in the iphone simulator, and use Weinre to run

app.stores.contacts.load()

I got this error:

TypeError: Result of expression ‘navigator.contacts’ [undefined] is not an object

I downloaded the complete source of this example from github and I have not made any changes. I use the latest phonegap (1.0.0) and the phonegap-1.0.0.js is clearly included in the index.html file.

Could someone tell me what is going on? Is it because the simulator doesn’t support ‘navigator.contacts’?

many thanks
Ben

Mark Walls

3 years ago

Hi, I don’t use a Mac - I know, blasphemy. smile Does touch work with Eclipse etc… or do I have to use Xcode to deploy a touch project with Sencha Touch?

Fandi

3 years ago

Hi guys,

Can you please help? I am using Eclispse and Android Emulator 2.3.3 but have problems with displaying ContactDetail.

I can get the ContactList to display - confirm this with putting alert in app/views/ContactsList.js:

onItemDisclosure: function (record) {
      alert(‘onItemDisclosure - yes’);
      Ext.dispatch({
        controller: app.controllers.contacts,
        action: ‘show’,
        id: record.getId()
      });
  }

But I cannot get the app.Controler.Contacts - show function to fire. I put following alert in app/controllers/contacts.js and it did not fire

  show: function(options) {
  alert(‘onShow - yes’);
  var id = parseInt(options.id),
      contact = app.stores.contacts.getById(id);
  if (contact) {
      app.views.contactDetail.updateWithRecord(contact);
      app.views.viewport.setActiveItem(
        app.views.contactDetail, options.animation
      );
  }
},

I don’t know what happen because I am really newbie in this space. But when I look at LogCat, I always see following warning immediately after clicking on ItemDisclosure.

“Window already focused. ignoring focus gain of: com.android.internal.view.IInputMethodClient$Strub$Proxy@406..”

Also, dont know if it is related or not, but after that warning soon after (sometimes immediately or up to 2 mins later) I usually see following Debug in LogCat:

“request time failed: java.net.SocketException: Address family not supported by protocol.”

Nicolas Sanitas

3 years ago

Thanks for this precious tutorial!
Perhaps I’m wrong, but does it possible to use the PhoneGap ChildBrowser plugin (https://github.com/purplecabbage/phonegap-plugins/tree/master/iPhone/ChildBrowser) for linking external websites; rather than editing the AppDelegate.m file?
(sorry, I am not able to test by myself for the moment)

jassi

3 years ago

Hi

Thanks for this brief tutorial.

I have done line by line as per tutorial but when i create app/app.js file and test on simulator it will shown following error please help me, i am new for phone gap and sencha touch.

ERROR:
[Switching to process 45631 thread 0x11803]
2011-09-14 09:58:53.135 emec[45631:f503] Device initialization: DeviceInfo = {“name”:“iPhone Simulator”,“uuid”:“369A6D1A-5E86-5DBD-B85D-676104574501”,“platform”:“iPhone Simulator”,“gap”:“1.0.0”,“version”:“4.3.2”,“connection”:{“type”:“wifi”}};

Antonio

3 years ago

Is there any example with Android and Eclipse?
I just need one info, where to put in Eclipse, and what files and folders, I need from compressed Sencha Touch file?

Leave a comment:

Commenting is not available in this channel entry.