Sencha Touch application with Ruby on Rails – Part 2

The source code for this application is posted on GitHub at https://github.com/hoitomt/Sencha-Tutorial

Sencha has formalized the MVC pattern recently as part of the ExtJS 4 release.  Check out this link for more information.

Create the Sencha Application

I’m back with the Sencha portion of the application.  We are going to create the application using the MVC (Model View Controller) pattern

Update your Rails App

In your rails public/javascripts directory create the following folders.

app

app/controllers

app/models

app/views

Add the sencha touch folder to the public/javascripts folder.  Take a look at the Hello World application from Sencha for some assistance on downloading the zip file containing all of the applicable files.  I downloaded it and extracted the entire package into public/javascripts/.  Then I renamed the folder touch_1_0_1.  So public/javascripts/touch_1_0_1 has the following directories: docs, examples, jsbuilder, pkgs, resources, src, and test.  It also contains some of the other files that come with the Sencha touch download.

Now copy the stylesheet over to a directory that Rails likes.  From public/javascripts/touch_1_0_1/resources/css copy “sencha-touch–debug.css” over to public/stylesheets/.  If you did the first part of this tutorial, then you will now have scaffold.css and sencha-touch-debug-css in the public/stylesheets directory.

Update your index file with the references to the Sencha Touch file and stylesheet.  Here is my index file.


<% if(mobile_device?) %>

 <!-- Code Viewable to mobile devices -->
 <%= content_for :head do %>
 <%= stylesheet_link_tag 'sencha-touch-debug' %>
 <%= javascript_include_tag 'touch_1_0_1/sencha-touch-debug' %>

 <style>
 .synced {
 background-color: #EDE613;
 }
 </style>

 <% end %>

<% else %>
 <%= content_for :head do %>
 <%= stylesheet_link_tag 'scaffold' %>
 <%= javascript_include_tag :defaults %>
 <% end %>
 <h1>Listing contacts</h1>

 <table>
 <tr>
 <th>First Name</th>
 <th>Last Name</th>
 <th>Email</th>
 <th>Phone</th>
 <th></th>
 <th></th>
 <th></th>
 </tr>

 <% @contacts.each do |contact| %>
 <tr>
 <td><%= contact.first_name %></td>
 <td><%= contact.last_name %></td>
 <td><%= contact.email %></td>
 <td><%= contact.phone %></td>
 <td><%= link_to 'Show', contact %></td>
 <td><%= link_to 'Edit', edit_contact_path(contact) %></td>
 <td><%= link_to 'Destroy', contact, :confirm => 'Are you sure?', :method => :delete %></td>
 </tr>
 <% end %>
 </table>

 <br />

 <%= link_to 'New Contact', new_contact_path %>

<% end %>

Create the Sencha App

Starting point: app.js

Now you should be ready to start building the application. Let’s start with the entry point into the application, the app.js file.  Put the app.js file into public/javascripts/app. The code below creates an instance of the viewport that will be used to display our panels. The Sencha API it give some helpful information about Ext.regApplication. The Sencha API will be your BFF as you build more applications. Creating your application with the regApplication function automatically creates your app.views, app.models, app.stores, and app.controllers namespaces which makes it easy to navigate through your application

Ext.regApplication(
  {
    name: 'app',
    launch: function() {
    this.views.viewport = new this.views.Viewport();
  }
});

Views: Viewport.js

Now let’s create the Viewport.js file.  Put the Viewport.js file in public/javascripts/app/views.  The code below sets up the main panel that will provide the navigation for all of your sub-panels.  When navigating between slides the code will actually be switching the active panel in the viewport.  The viewport below shows all of the panels in the application.  We haven’t created these yet so your application will crash if you try to run it right now.  You can go ahead and create empty files for the 5 files below (ContactsList, ContactShow, ContactEdit, ContactNew, and ContactCatalog).  Put them all into public/javascripts/app/views.

//app.views.Viewport
app.views.Viewport = Ext.extend(Ext.Panel, {
  fullscreen: true,
  layout: 'card',
  cardSwitchAnimation: 'slide',
  initComponent: function() {
    Ext.apply(app.views, {
      contactsList: new app.views.ContactsList(),
      contactShow: new app.views.ContactShow(),
      contactEdit: new app.views.ContactEdit(),
      contactNew: new app.views.ContactNew(),
      contactCatalog: new app.views.ContactCatalog()
    });
    Ext.apply(this, {
      items: [
        app.views.contactsList,
        app.views.contactShow,
        app.views.contactEdit,
        app.views.contactNew,
        app.views.contactCatalog
      ]
    });
    app.views.Viewport.superclass.initComponent.apply(this, arguments);
  }
});

Update your index file (app/views/contacts/index.html.erb) to add the necessary imports.  Then you can run your application and you should see the empty ContactsList panel displayed.

<%= javascript_include_tag 'app/app' %>
<%= javascript_include_tag 'app/views/ContactCatalog' %>
<%= javascript_include_tag 'app/views/ContactEdit' %>
<%= javascript_include_tag 'app/views/ContactNew' %>
<%= javascript_include_tag 'app/views/ContactShow' %>
<%= javascript_include_tag 'app/views/ContactsList' %>
<%= javascript_include_tag 'app/views/Viewport' %>

Model: Contact.js

Now let’s take a look at the model. Create Contact.js in public/javascripts/app/models. Our model serves three purposes:

1. Get data from the web server for the application

2. Store data locally in localstorage

3. Post data to the web server that has been captured on the device.

We’re going to do this with multiple proxies.  The Sencha proxies make it easy to persist data.  The code below defines the fields, validations, and a proxy for saving a single model instance to the web server.  I like using xml to transmit back and forth but I notice that the Sencha guys like using json. XML is more comfortable for me at the moment and it seems to work well with ROR.

//Contact.js
app.models.Contact = new Ext.regModel('app.models.Contact', {
  fields: [
    {name: 'id', type: 'int'},
    {name: 'remote_id', type: 'int'},
    {name: 'synced', type: 'boolean'},
    {name: 'first_name', type: 'string'},
    {name: 'last_name', type: 'string'},
    {name: 'email', type: 'string'},
    {name: 'phone', type: 'string'}
  ],
  validations: [
    {type: 'presence', field: 'first_name'},
    {type: 'presence', field: 'last_name'}
  ],
  proxy: {
    type: 'ajax',
    url: 'contacts.xml',
    reader: {
      type: 'xml',
      record: 'contact'
    },
    writer: {
      type: 'xml',
      record: 'contact'
    }
  }
});
...

Now add two more stores to your model.  The first one is a local store which will help with CRUD to your local storage.  The second is a remote store that we use only to get a complete list of data from the web server to populate our catalog.

//Contact.js
...
app.stores.localContacts = new Ext.data.Store({
  id: 'localContacts',
  model: 'app.models.Contact',
  proxy: {
    type: 'localstorage',
    id: 'contacts'
  }
});

app.stores.remoteContacts = new Ext.data.Store({
  id: 'remoteContacts',
  model: 'app.models.Contact',
  proxy: {
    type: 'ajax',
    url: 'contacts.xml',
    reader: {
      type: 'xml',
      root: 'contacts',
      record: 'contact'
    },
    writer: {
      type: 'xml',
      record: 'contact'
    }
  }
});
...

Finally we need to add the CRUD and synchronizing code. The first function app.models.save is called from the controller when the save button is clicked on the ContactNew form.  This takes all of the data from the form and persists it into local storage using the local store.  The stores work really well with models so one trick that I use is to create a new model using the data from the form.  Ext.ModelMgr.create(data, modelname) makes it easy to pass in a list of name-value pairs to create a new model instance.  The store has a really nice method (add) for persisting existing models.  Together you can pull all of your form data and persist it with only a couple of lines of code.

// Contact.js
...
app.models.save = function() {
  var form = app.views.contactNew
  var params = form.getValues();
  var newcontact = Ext.ModelMgr.create(params, app.models.Contact);
  var errors = newcontact.validate();
  if (errors.isValid()) {
    app.stores.localContacts.add(newcontact);
    app.stores.localContacts.sync();
    form.reset();
    return true;
  } else {
    var errorMsg = '';
    errors.each(function(e) {
    errorMsg = errorMsg + fieldHumanize(e.field) + ' ' + e.message + "<br />";
    errorMsg = errorMsg ;
  });
  Ext.Msg.show({
    title: 'Error',
    msg: errorMsg,
    buttons: Ext.MessageBox.OK,
    fn: function() {
      return false;
    }
  });
}

Next we write the update function. This is called from the controller when the update button is clicked from the ContactEdit screen. All of the data is pulled from the form into a params variable.  The code then loops through the params and updates everything on the model.

...
app.models.update = function(id) {
  var contact = app.stores.localContacts.getById(id);
  if(contact) {
    var form = app.views.contactEdit
    var params = form.getValues();
    for(var field in params) {
      console.log("field: " + field + ' | value: ' + params[field]);
      contact.set(field, params[field]);
    }
    var errors = contact.validate();
    if(errors.isValid()) {
      contact.set('synced', false);
      app.stores.localContacts.sync()
      Ext.Msg.alert('Updated', 'The contact has been updated');
      return true;
    } else {
      var errorMsg = '';
      errors.each(function(e) {
      errorMsg = errorMsg + fieldHumanize(e.field) + ' ' + e.message + "<br />";
      errorMsg = errorMsg ;
    });
    Ext.Msg.alert('Error', errorMsg);
      return false;
    }
  } else {
    return false;
  }
}

Next a short delete function to remove the record from local storage only.

app.models.destroy = function(id) {
  var contact = app.stores.localContacts.getById(id);
  if(contact) {
    app.stores.localContacts.remove(contact);
    app.stores.localContacts.sync();
    return true;
  } else {
    return false;
  }
}

Finally the sync function. After validating that the device is online the localstore is loaded with the latest data from localstorage.  Each record in the local store is evaluated to find only those records that need to be synced (synced == false).  Once we have the sync array and there are more than 0 records in the syncArray we loop through it.  NOTE: I’m using a for loop but it is actually better to use an each loop when going through the array.  To do that you would replace for(var i=0….) with syncArray.each(function(form) {… existing code …}.  Then remove the line form = syncArray[i].

The pattern for syncing is as follows: use the data from the syncArray record to create a new model.  Then use the proxy on the app.models.Contact model to save the record to the web server.  This is why we have a proxy on the model, so that we can call syncModel.save and have the data synchronize with the server.  There are three callbacks: success, failure, and callback.  Success is called if the data successfully saves.  Failure is called if something goes wrong and Callback is called regardless of what happens.

One of the tricky things with Javascript and ExtJS is managing the asynchronous actions.  That is why the callbacks are used.  If you just call syncModel.save() and then try to pop a success message, it won’t work the way you think it would.  The save action will execute asynchronously and the code will move onto the next line while it the save is still executing.  So your alert message would display even though the save hadn’t finished.

app.models.synchronizeLocalToRemote = function () {
  if(!navigator.onLine) {
    Ext.Msg.alert('Offline', 'You need to be online to sync to the web server');
    return;
  }
  console.log('Start Sync');
  var localStore = app.stores.localContacts.load();
  var syncArray = getDataToSync(localStore);
  var count = syncArray.length;
  if(count == 0) {
    Ext.Msg.show({
      title: 'Synced',
      msg: 'All contacts are synced'
    });
    return;
  }
  var syncInfo = "";
  console.log("Number of items to sync: " + count);

  // Show the syncing spinner
  var mask = new Ext.LoadMask(Ext.getBody(), {msg: "Synchronizing"});
  mask.show();

  // Sync items to remote
  for(var i = 0; i < count; i++) {
    console.log("Index: " + i);
    form = syncArray[i];
    var syncModel = Ext.ModelMgr.create(form.data, app.models.Contact);
    // Calling save on the model calls the remote proxy
    syncModel.save({
      success: function(result, request) {
        var id = result.data['id']
        console.log("Result ID: " + id);
        console.log("Success");
        form.set('remote_id', id);
        form.set('synced', true);
        localStore.sync();
        syncInfo = 'Success: ' + form.get('first_name') + ' ' +
        form.get('last_name') + ' has been synced<br />';
      },
      failure: function(result, request) {
        console.log("Exception");
        console.log("Result: " + request.responseText);
        syncInfo = 'Failed: ' + form.get('first_name') + ' ' +
        form.get('last_name') + ' has been synced<br />';
      },
      callback: function(result, request) {
        console.log(syncInfo);
        if(i >= count - 1) {
          mask.hide();
          Ext.Msg.show({
            title: 'Complete',
            msg: syncInfo
          });
        }
      }
    });
  }
}

var getDataToSync = function(store) {
  var syncArray = new Array();
  store.each( function(form, index) {
    var isSynced = form.get('synced');
    if (!isSynced) {
      syncArray.push(form);
    }
  });
  return syncArray
}

Phew! Are you still with me? The code for the views is relatively straight-forward. I’ve posted the source code on GitHub so you can get walk through the views (public/javascripts/app/views) there.  However I want to walk through a couple of examples of following a click from the view through the controller to another view.

Views and Controller: contacts.js

Here is a snippet from ContactsList.js.  This is the button and handler for the “new” button on the top toolbar.  When you click on the ‘new’ button on the toolbar the framework will take you to the controller.

//ContactsList.js
...
{
  text: 'new',
  ui: 'confirm',
  handler: function() {
    Ext.dispatch({
      controller: app.controllers.contacts,
      action: 'newContact',
      animation: {type: 'slide', direction: 'left'}
    });
  }
}
...

The controller is in public/javascripts/app/controllers and is called contacts.js. From the newContact action in the controller we can see that the controller is setting the active panel on the viewport to the contactNew panel. One thing to note: it is setting the active panel to an instance of the new panel.  Notice the lower-case “c” on contactNew.  We are using the instances of the panels that were created in the Viewport code.

...
newContact: function(options) {
 app.views.viewport.setActiveItem(app.views.contactNew, options.animation);
},
...

A more complex example of moving from panel to panel involves selecting an item from the list. From the ContactList panel you can click on an item to edit it.  There are two click handlers:  onItemDisclosure handles clicks directly on the arrow and onItemTap handles clicks on the row.  They both do the exact same thing except that onItemDisclosure has access to the actual records whereas onItemTap has to translate the record from the item.  The code below should look familiar as compared to the code above with one significant addition: We are sending an id to the controller.  The id and the animation are both transmitted to the controller in an ‘options’ object.

//ContactsList.js
...
onItemTap: function(item) {
  record = this.getRecord(item);
  Ext.dispatch({
    controller: app.controllers.contacts,
    action: 'show',
    id: record.getId(),
    animation: {type: 'slide', direction: 'left'}
  });
}
...

The controller uses the id from the options hash to retrieve the correct record for editing.  If a contact is found in localstorage then the contactEdit view is updated with the data.  The panel is updated by calling a method in the contactEdit view called updateWithRecord

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

It is a little bit magical actually. As long as you are using a FormPanel it is really easy to update the fields with data from an existing record.  By calling the ‘load’ method (this.load(record)) on the FormPanel and passing it the record each of the fields is mapped to the record object using the ‘name’ of the field.  For example if the first_name of the record is populated it will automatically fill in the textfield where name: ‘first_name’.  It will do this for all fields where the name equals the field from the record.  It is good to keep this in mind when designing your forms: Make sure to name your fields that same as your model fields.

// ContactEdit.js
...
updateWithRecord: function(record) {
  this.load(record);
  var topToolbar = this.getDockedItems()[0];
  topToolbar.getComponent('cancel').record = record;

  var bottomToolbar = this.getDockedItems()[1];
  bottomToolbar.getComponent('save').record = record;
  bottomToolbar.getComponent('save').form = this;
}

Make sure to add references to the controller and model in your index.html.erb.  Take care to add them in the following order. The Sencha framework will throw an error if the data for the ContactList is not available when you try to display the list. So it is important to load the model first, then the views.

<%= javascript_include_tag 'app/app' %>
<%= javascript_include_tag 'app/util' %>
<%= javascript_include_tag 'app/models/Contact' %>
<%= javascript_include_tag 'app/views/ContactCatalog' %>
<%= javascript_include_tag 'app/views/ContactEdit' %>
<%= javascript_include_tag 'app/views/ContactNew' %>
<%= javascript_include_tag 'app/views/ContactShow' %>
<%= javascript_include_tag 'app/views/ContactsList' %>
<%= javascript_include_tag 'app/views/Viewport' %>
<%= javascript_include_tag 'app/controllers/contacts' %>

That is pretty much it for our application.  There are some fun things in the source code, like a catalog where you can view a list of items from the server on your mobile device.  Then touch the item to download it to the device for manipulation.  Please post any questions or comments and I’ll try to help out.

Thanks for reading!

Advertisements

19 thoughts on “Sencha Touch application with Ruby on Rails – Part 2

  1. super nice write_up!

    I am, however, stuck at ‘hello’ 😦

    ie. using the KitchenSink (which has no MVC) I’m trying to cross from the NestedList to the – say Contacts – controller action index.

    And close to a week of googling/trying/reading sencha-touch.js has not gotten me anywhere closer!

    Can you get me there?

    Cheers,
    Walther

    • Hi Walther,
      Go all the way through the Nested List demo in the kitchen sink, down to where the cars are named (Acura, Honda, Infiniti, etc… in the Japan category). Once you reach the end of the tree the edit button is enabled. You can see the code for the edit button in the source code under var editBtn = new Ext.Button. In there the handler: function() { … } has the code for retrieving the item from the list. First they assign the actual list to a variable activeList, then they get the record from the list. Once you have that record, you can call Ext.dispatch with the controller and actions. Then set the record id in the options. Similar to this:
      Ext.dispatch({
      controller: app.controllers.serviceForms,
      action: ‘editForm’,
      id: record.getId(),
      animation: {type: ‘slide’, direction: ‘left’}
      });

      • Hi hoitomt,
        it’s so good of you to get back to me so quickly! Thank you very much.

        I should have included that in my original post 😦

        I’ve tried that already –

        this is my Structure.js in app/stores

        ….
        items: [
        {
        text: ‘countries’,
        card: ‘oxenTouch.controllers.countries’,
        action: ‘index’,
        leaf: true
        }
        ]

        this is my app/controllers/country.js

        Ext.regController(“countries”, {

        index: function() {
        if (!this.listPanel) {
        this.listPanel = this.render({
        xtype: ‘country-listpanel’,
        listeners: {
        ……

        this is my app/views/Country/CountryListPanel.js

        oxenTouch.views.CountryListPanel = Ext.extend(Ext.Panel, {
        fullscreen: true,
        layout: ‘card’,
        backText: ‘Back’,
        useTitleAsBackText: true,
        initComponent : function() {
        this.store = oxenTouch.stores.Country;

        this.list = new Ext.List({
        itemTpl: ‘{Name}‘,
        store: this.store,
        grouped: false,
        indexBar: false
        });

        this.items = [this.list];

        oxenTouch.views.CountryListPanel.superclass.initComponent.call(this);
        },

        });

        Ext.reg(‘country-listpanel’, oxenTouch.views.CountryListPanel);

        this is my app/stores/country.js

        oxenTouch.stores.Country = new Ext.data.Store({
        autoLoad: true,
        model: ‘Country’,
        sorters: [‘Name’],
        getGroupString: function(record) {
        return record.get(‘Name’)[0];
        }
        });

        and my app/models/Country.js

        Ext.regModel(“Country”, {
        fields: [

        {name: “id”, type: “int”},
        {name: “name”, type: “string”},
        {name: “abbreviation”, type: “string”}
        ]
        });

        and this is the OnNavPanelItemTap in my viewport

        …..

        if (card) {
        act = record.get(‘action’);
        console.log(card);
        Ext.dispatch({
        controller: card,
        action: act,
        animation: {type: ‘slide’, direction: ‘left’}
        });
        }
        ….

        – and I’ve tried hardcode the oxenTouch.controllers.countries for the controller like

        Ext.dispatch({
        controller: oxenTouch.controllers.countries,
        action:’index’,
        animation: {type: ‘slide’, direction: ‘left’}
        });

        It just woun’t work with me :/

        Cheers,
        Walther

      • I see two spots where you’re calling Ext.dispatch. In both of them it doesn’t look like the record id is being passed into the controller. I might be missing it but you need to pass the record id with something like id: record.getId(). Then in your controller you pass in the options hash and use the following to retrieve the id and the corresponding record.
        var id = parseInt(options.id);
        var product = app.stores.localProducts.getById(id);

        So a show action in the controller would look like

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

  2. ehh – I’m afraid I do not express myself clearly!

    I use the same navigation layout as KitchenSink – with a navigationbar on top, a navigationpanel on left (with a nestedlist) and content, well that is the problem! The nestedlist on the left draws data from a Structure.js which is a nested object, and once a ‘leaf’ is reached, the nestedlist “activates” a src-file!!!??? How that flies is beyond me – but that is not the problem.

    My problem is that instead of calling this src-file (like eg. demos/list.js) – I’d like to activate app.controllers.countries with the index action

    – is this just exposing what little I understand of the Sencha Touch Framework or?

    Sorry to keep you occupied with this ridiculous issue – I seem to be the only one having this problem, so I clearly must be misunderstanding the entire concept <:(

  3. got it 🙂

    answer was to ‘launch’ the app at a dashboard controller which in turn would use your dispatch

    why didn’t I think of this a week ago <:)

    thanks for your help

    cheers,
    Walther

  4. Mike, this is so awesome! Thank you so much for taking the time to record your experiences with working with Sencha Touch.

    I do have one question: I understand why you need to have the localContacts and remoteContacts store, with their respective proxies, but what is the purpose of the additional proxy on the Contact model itself? Thanks for any info you could provide.

    • Hi, It is a bit confusing because I have both the read and write on both “remote” proxies. The proxy that is associated with app.models.Contact is used to push data to the remote host. You can see it in synchronizeLocalToRemote when I call syncModel.save.

      app.models.remoteContacts is used to populate the list of contacts. You can see it used in the contacts controller under the catalog function

  5. HI, I am newbie for sencha. Actually I am an ror developer. I have developed mobile application in rhodes and now trying to do in sencha.
    I have done all steps you have given in part1 and part 2.
    When I hit my url from web [http://localhost:3000/] I get view as per my erb file and when I hit same url from android emulator I get nothing and server log shows execution of erb file. I got no errors of including java script files.
    How I do start application in mobile?

    • Hi, I haven’t actually tried it from an Android emulator. I’ve tried it from an iPod touch and from the mobile user agent in Safari. Try to view it in Safari with the user agent set to mobile (Develop -> User Agent). If it isn’t showing up in there, then I’m not sure what is happening.

      Mike

  6. Hi I tried a lot with sencha touch 2 but now moving to sencha touch 1.1 and got succes.
    I want to get and send data as json . in model if I change xml to json then I am able to get it but there is nothing displayed on UI and I get error at console as
    TypeError: Result of expression ‘a’ [undefined] is not an object. in sencha-touch.js:6. Thanks.

Leave a Reply

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

WordPress.com Logo

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

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s