Showing posts with label backbone. Show all posts
Showing posts with label backbone. Show all posts

Wednesday, February 22, 2012

Using jQueryMobile and Backbone.js for handling forms

Introduction

In this post, I continue the development of my basic "exercise app" that I started (and enhanced) in these posts:

Let's add the ability to create and edit exercise records.

Adding a new activity

Just like in the previous post, the first thing we need is a jQueryMobile page to hold the form content. Add the following to index.html:

    
Save

New Activity

In the code above, we see the typical jQueryMobile page. The main things of interest here are with respect to the anchor tag in the header div:

  • Specify data-rel="back" to navigate to the previous page once the save is complete. This way whether we enter the form page from "Add" or "Edit", navigation will be handled.
  • The class="ui-btn-right" moves the button to the right side of the header bar.
  • The data-theme="b" causes the save button to use the blue theme.
The next step is to define the template that the view will use to render the content for this page. Lets start with a simple template with an input for each attribute of an activity. To vary the UI slightly, we can use a drop down for the activity type. The basic template will look like this:

    

Notice that we are using the HTML 5 input types for some of the fields. Browser support for these will vary, but worst case they will deprecate to a text input type. For example, on iOS, the date will present a native date picker, whereas on Android (or Chrome, etc.) the field will behave as a standard text input.

Now we need to navigate to the form page. This requires modifying the "Add" button on the list page.

    

Activities

Add

In order to transition to the form page, the href attribute on the anchor tag (i.e. button) needs to be changed to the id of the form page, as shown in line 4 above. The final step in presenting the form page to support adding exercise activities is modifying the add button click handler.

    $('#add-button').live('click', function(){
        var activity = new exercise.Activity(),
            activityForm = $('#activity-form-form'),
            activityFormView;

        activityFormView = new exercise.ActivityFormView({model: activity, viewContainer: activityForm});
        activityFormView.render();
    });

Here we create an empty model, grab the form node, create a new exercise.ActivityFormView with these objects, and render the form. Testing at this point reveals a problem. The screen shot below doesn't look right ;)

Investigating the browsers console reveals an error: Uncaught ReferenceError: distance is not defined. This is due to the underscore template trying to access an undefined attribute of the model. The issue is described here with some work arounds. I found the easiest workaround is to provide defaults for the model, as shown below.

     exercise.Activity = Backbone.Model.extend({
        defaults: {
            date: '',
            type: '',
            distance: '',
            comments: '',
            minutes: ''
        }
     });

Now the form renders properly. With the help of some CSS, we can tighten things up a little and end up with the version on the right (the CSS is part of the source code, see the links at the end of this post).

Editing an existing activity

With the template and the view already implemented, adding edit functionality is pretty straightforward. The first step is to update the href attribute of the edit button on the activity-details page.

    
Edit

Activity Details

As we did with the add button, this should reference the activity-form page. Now we need to add the click event handler for the edit button.

    $('#edit-activity-button').live('click', function() {
        var activityId = $('#activity-details').jqmData('activityId'),
            activityModel = exercise.activities.get(activityId),
            activityForm = $('#activity-form-form'),
            activityFormView;
        
        activityFormView = new exercise.ActivityFormView({model: activityModel, viewContainer: activityForm});
        activityFormView.render();
    });

If you recall, the activityId is passed to the details page view the list item click event handler. We reuse that fact here to retrieve the activity from the collection.

Time to test. Selecting an activity renders the detail page. Clicking the edit button renders the form, pre-filled with the activity details. Everything looks great.

But wait.....that screenshot was from the browser on which I am testing. We should always test on devices as well. Earlier I mentioned that we are using some of the HTML 5 input types (i.e. date). The nice thing is that iOS 5 will render a nice date picker for date types. The challenge is that the date needs to be in a specific format for that to work. The image below shows what the form page looks like on an iOS device.


Fixing the Date

No date value is showing up on our iOS device. This is a known issue. So, for iOS we need the date format as yyyy-mm-dd, while for other platforms, I prefer the date to be shown as mm/dd/yyyy. I do realize that date formats should be localized, but for the purpose of this example I would like to keep it straightforward and specify mm/dd/yyyy as the display date format. There are many options that could be pursued here, but to demonstrate some more capabilities of Backbone.js, we will modify our model to help meet our date requirements.

First, the date attribute in our JSON feed is a string. Lets convert this to a date when the data is retrieved from the server, then when we need the date, we can just manipulate it as needed. This can be accomplished several ways.

  • Implement a parse method on the collection that converts the String to a Date as the data is being fetched from the server.
  • Implement a method on the model that converts the date String to a Date and sets it on the model. This would then require the calling code to know to call this method depending upon the circumstance.
  • Override the set method on the model, look for the date attribute, then convert it to a date as needed.
The last option is the most seamless approach. Add the following method to the Activity model.

        set: function(attributes, options) {
            var aDate;
            if (attributes.date){
                //TODO future version - make sure date is valid format during input
                aDate = new Date(attributes.date);
                if ( Object.prototype.toString.call(aDate) === "[object Date]" && !isNaN(aDate.getTime()) ){
                    attributes.date = aDate;
                }
            }
            Backbone.Model.prototype.set.call(this, attributes, options);
        }

Also, update the defaults so that the date attribute is now a Date instead of a String.

        defaults: {
            date: new Date(),
            type: '',
            distance: '',
            comments: '',
            minutes: ''
        },

While this clearly isn't the most robust date handling, it is fine for now. In a future version, this should be improved.

Next, add attributes to our model that will format the date in the ways we need it (i.e. mm/dd/yyyy and yyyy-mm-dd).

        dateInputType: function(){
            return exercise.formatDate(this.get('date'), "yyyy-mm-dd"); //https://github.com/jquery/jquery-mobile/issues/2755
        },
        
        displayDate: function(){
            return exercise.formatDate(this.get('date'), "mm/dd/yyyy");
        }

The formatDate function is a simple date formatter to meet our specific needs and can be found in the source code that accompanied this post (see link at the bottom of the post).

Now, how do we use these new methods in our view. The first thing to realize is that we pass the template JSON. The default implementation of the toJSON method of a Backbone.js model will not include these functions. Therefore, we need to override the toJSON method.

        toJSON: function(){
            var json = Backbone.Model.prototype.toJSON.call(this);
            return _.extend(json, {dateInputType : this.dateInputType(), displayDate: this.displayDate()});
        }

Here, we are using the Underscore.js extend to add our attributes to the standard Backbone JSON. Now, we need to modify our view templates to use the appropriate JSON attributes.

    
    
    
    
    

Notice that lines 16-21 include some conditional logic. This is a very basic device detection check to determine which date format to use. There are plug-ins,etc. that provide more robust alternatives, but to keep things clear, this will serve our needs. The screen shots below show the desktop browser and the iOS versions of the form with the appropriate date handling.

Saving

The final step is to implement the save functionality.

    $('#save-activity-button').live('click', function(){
        var activityId = $('#activity-details').jqmData('activityId'),
            activity,
            dateComponents,
            formJSON = $('#activity-form-form').formParams();
        
        //if we are on iOS and we have a date...convert it from yyyy-mm-dd back to mm/dd/yyyy
        //TODO future version - for non-iOS, we would need to validate the date is in the expected format (mm/dd/yyyy)
        if (formJSON.date && ((navigator.userAgent.indexOf('iPhone') >= 0 || navigator.userAgent.indexOf('iPad') >= 0)) ){
            dateComponents = formJSON.date.split("-");
            formJSON.date = dateComponents[1] + "/" + dateComponents[2] + "/" + dateComponents[0];
        }
        
        if (activityId){
            //editing
            activity = exercise.activities.get(activityId);
            activity.set(formJSON); //not calling save since we have no REST backend...save in memory
        }else{
            //new (since we have no REST backend, create a new model and add to collection to prevent Backbone making REST calls)
            activity = new exercise.Activity(formJSON);
            activity.set({'id': new Date().getTime()});  //create some identifier
            exercise.activities.add(activity);
        }
    });

I used the JavascriptMVC formParams jQuery plugin (found here) to convert my HTML form into a JSON object. Then, the date is converted into the appropriate format. One thing to note here is that since we have no real back end server for Backbone to interface with, we don't call save on the model or create on the collection. These methods would cause the appropriate REST calls to the server.

Since the click event on the list view passes the activityId to the details page, we can use this to determine if we are adding or editing. One thing to consider here is that the user may click an existing activity, then go back to the list, then click add. In this case, the activityId from the previously selected activity is still attached to the activity-details view. This will cause our save implementation to think we are editing. To prevent this, we should remove the activityId from the activity-details page when adding a new activity. This can be accomplished by adding the highlighted line below to the add handler.

    $('#add-button').live('click', function(){
        var activity = new exercise.Activity(),
            activityForm = $('#activity-form-form'),
            activityFormView;
    
        //clear any existing id attribute from the form page
        $('#activity-details').jqmRemoveData('activityId');
        activityFormView = new exercise.ActivityFormView({model: activity, viewContainer: activityForm});
        activityFormView.render();
    });

The final step is to make sure our views are updated accordingly when we add or modify activities. Since the details view retrieves the activity before the page loads, no change is required here. But our list view only renders during the initial page load. This is where we can take advantage of Backbone event binding. A few small additions to the ActivityListView and things will be handled.

        initialize: function() {
            this.collection.bind('add', this.render, this);
            this.collection.bind('change', this.changeItem, this);
            this.collection.bind('reset', this.render, this);
            this.template = _.template($('#activity-list-item-template').html());
        },

        ...

        changeItem: function(item){
            this.collection.sort();
        }

The change event will bubble up from the model and fires as a result of the set being called. In the changeItem method, we sort the collection to handle any changes in the date attribute. The sort method call will cause the reset event to fire, which then results in the render method invocation, causing the list to be re-rendered. This keeps everything in the correct order. The add event will be fired when we add a new activity to the collection. Since we implemented the comparator method on the collection, models added to the collection will be added in the appropriate order.

We can now add and edit activities. Start exercising :)

The source code for this post can be found here

Monday, January 30, 2012

From a List to a Details View using jQueryMobile and Backbone.js

In my previous post I built a basic application to demonstrate the use of Backbone.js with jQueryMobile (JQM). The introduction can be found here, with a brief subsequent post on sorting collections here. In this post, I would like to add the capability to view the details of the items presented in the list view.

The first step is to create a new JQM page to display the details view. JQM makes it pretty easy to add pages to your application. I added the following code to the index.html file:

This will create the structure for the page, and Backbone.js will be used to fill in the content div based on the record tapped (or clicked) within the list view. The next step is to define the template for the details view. This is the pattern that I follow when developing using jQueryMobile and Backbone.js. The template can be inserted below the list item template in index.html.

I decided to embed the details in a read-only list view, this way jQueryMobile will provide some reasonable styling. Since the point here is to demonstrate Backbone and jQueryMobile playing together, I didn't want to have to spend much time on style :)

Next, we need to define the Backbone view that will use the template to render the appropriate content. All this View needs to do is apply the model to the template and append it to the HTML container defined when the View is instantiated.

In order to retrieve the correct model to bind to the details view, we need to know what row in the list view was clicked (or tapped). To do this, we can bind to the click event of the item in the list view. This can be accomplished by modifying the ActivityListView render method. Here is the current version of the list view:

The key area to focus on here is lines 18-20, where the activity item HTML is rendered and appended to the list view. This is where the modification needs to occur. Each activity item HTML element needs to be bound to a click event. In this click event, the activity id will need to somehow be passed to the details view so the appropriate look up can occur. There are several ways to do this. Approaches I have used in the past include the use of jQuery.jqmData(...) or session local storage (assuming HTML5). In this example, we will use the jqmData method. The trick is to capture the id during the rendering of the list so that it can be used during the execution of the click event. Below are the required modifications to lines 18-20 of the previous code snippet.

The first thing is to capture the rendered activity item HTML in a variable and cast it to a jQuery object, as can be seen in lines 2-3. Line 4 is where the activity id data is attached to our activity item HTML element. Then, the bind event (lines 5-8) retrieves the attached data and sets it on the activity details HTML element, which is our activity details page HTML. Per the jQuery documentation, this within a bind method refers to the DOM element to which the event handler is bound. We can use that to get at the activity id data and attach it to the details view. This gives us the ability to pass the appropriate id at runtime to the details page.

Now we need to wire all this together. The typical pattern I follow is to use the jQueryMobile pagebeforeshow event to set up everything needed to render a complete page. This acts as my controller.

This code retrieves the id data attached to the activity details element, looks up the model using the Backbone API (line 5), instantiates the view with the model and the view container, and calls the render method to render the final HTML. The end result looks like this:

The source code for this post can be found here. Note that this is a branch of my repository for this sample app. Each blog post associated with this code base resides on a separate branch.

Related Posts:

Tuesday, January 3, 2012

Introduction to Backbone.js with jQuery Mobile

If you are working on a JavaScript heavy application (think jQuery Mobile, etc.), you probably will want to look at some JavaScript libraries to help add structure, consistency and convenience to your applications. One of the JavaScript libraries I've used lately is Backbone.js. To quote Backbone themselves, it provides "models with key-value binding and custom events, collections with a rich API of enumerable functions, views with declarative event handling, and connects it all to your existing application over a RESTful JSON interface.". This is really a great summary of what Backbone can provide to your application.

As an example of how to apply Backbone, I'll create a small jQuery Mobile app and use a Backbone model, collection and view to render the user interface. Through the use of Backbone's event model, the user interface will update as the model changes or new models are created.

I decided to use jQuery Mobile as the UI framework mainly because I am a mobile guy and it will provide the HTML with some reasonable style with no additional effort on my part. I know, lazy...but it works for this blog post :) Continuing on the lazy path, I will refer to jQuery Mobile as JQM from this point forward.

The following assumes you are at least a little familiar with jQuery and JQM. If not, take a look at the JQM website (http://jquerymobile.com/). They have some great documentation and even a template for creating a basic app. All of the code in this post will be available in my github repository (https://github.com/stevenpsmith/Exercise/tree/BackboneIntro).

The application we are going to make will allow a user to log exercise they have done on any given day. Please remember this is a very basic app for the sole purpose of demonstrating Backbone.js. The piece that will be developed for this post will involve pulling exercise data from a server and presenting it in a list view, a very common paradigm for mobile apps. In the end, our app will look like this:

The JSON we will be dealing with will look like this:

I like to organize my application structure a little differently than the JQM introduction shows, mainly for organizational reasons. My application structure looks like this:

All of our work will be done in index.html and app.js. In real applications I usually break down my JS files even further, but it really isn't necessary here. The list page of the application is defined within index.html. It should look like a typical JQM page definition with the only the content portion defined. We will use the Backbone View to create the list view:

If we view this in the browser, we get an empty page with an add button in the header. Since we want to populate our list view with data, we should define a Backbone model and collection in order to manipulate and store the data via JavaScript. A basic model and its collection are easily defined:

The great thing about the collection is it provides a RESTful access back to your server for data retrieval and persistence. To keep this simple, our data will be served from a static JSON file with the structure referenced earlier. Since we have no real server side in this example, we can't really save our changes anywhere but in the client's memory. But the collection will fetch the data from the server and can be used to help render our user interface. The following code will create an instance of the collection in memory, retrieve the data from the server, and parse it into a collection of models.

The exercise.initData() function can be called during initialization of the application (or wherever else might be appropriate) to load the data. Now it is time to render the view. Backbone views depend upon Underscore.js. Underscore is a utility library that includes a lot of great features, one of which is view templates. Backbone can be used with many view template frameworks (e.g. Mustache, Handlbars, etc.), but here we will stick with Underscore templates as they meet our needs. For our exercise activity items, we need a very basic template like this:

Giving the template a unique id will let us access it using typical jQuery selectors. The data driven parts of the template are surrounded by

<%= %>
As you can see in line 2, we are using the id, date, and type attributes of whatever model is supplied to the template to fill in the HTML accordingly. While the "identifier" attribute is not a real HTML attribute and not required by any of the frameworks, it would be used to determine what activity the user selected in the list so the appropriate data could be retrieved and displayed on a subsequent page.

The next step is to define the Backbone View that will render the list view. Backbone views can be defined and set up numerous ways, and the documentation does a great job explaining the options, etc. I like to define my views so that they are as self contained as possible. So, rather than allowing Backbone to create the HTML within a DIV (the default behavior), I define the appropriate properties to create a JQM list view as shown in lines 2-4 below:

From the Backbone site: "The default implementation of render is a no-op. Override this function with your code that renders the view template from model data, and updates this.el with the new HTML. A good convention is to return this at the end of render to enable chained calls". The $(this.el) refers to the containing element, in this case the ul element. There are standard attributes that are accessible from within the Backbone View including collection and model. Lines 17-20 iterate over the collection of activities, fills in the template placeholders with the data from the model, and appends the resulting HTML to the containing element.

If you need to pass arbitrary data to your view, it can be accessed via the options object as shown on line 11 of the code snippet. As mentioned earlier, I like my Backbone Views to be as self contained as possible, so I provide the containing HTML element for this view (as a jQuery object) when constructing the View instance. Line 20 sets the HTML of the containing element to the Backbone View and line 21 tells JQM to style things appropriately.

The next step is to instantiate and render the view with the appropriate collection and HTML container. Since this is the "home" page in our application, this is done after JQM initializes the page.

Now we have a list view that renders a JQM list based on data in a Backbone collection. To take advantage of the real power of Backbone models/collections, we can add listeners for changes to the collection that will update the view based on those changes. The beauty of this is the additional code to accomplish this is minimal. By adding

this.collection.bind('add', this.add, this);
to the initialize method of our Backbone View and providing an add() method.

Here we are just grabbing the list view and appending the new model to the view (and refreshing it so JQM does its magic). By adding a click handler to the "Add" button, we can test that adding a model to the collection updates the view.

There are other methods on which to bind, but this provides a quick example on how Backbone can be used to assist with view rendering and data handling in JavaScript based client applications.

The entire code base is available here. It can be deployed on any web server. For OS X, I use the built in web server, but feel free to use the server of your choice.

Related Posts: