Tuesday, September 23, 2014

Google Maps Displaying Custom Salesforce.com Records

The problem: Provide an easy reference to a list of customers with our product (in the amusement industry) to prospective customers so they can personally see the product in action. In the selling process the prospective customer wants to see the product in action and wants the location of a product model close by to go and check it out. In the past, the sales people would look at a list and try and find the correct product model as close as possible and pass that information along to the customer. To say the least it was not a very efficient process, nor could the prospect see the many other product models over 5 continents.

From my perspective the solution was the use of Google Maps, but we could hardly put our customer list on Google Maps. The solution was two-fold. The first was to create a custom application in Salesforce.com that contained all of the information about the products sold to customers. The second portion was to extend this information to Google Maps in a controlled manner using different criteria to control the information displayed.

This blog article is not going into the building of the custom application in Salesforce.com, but the extension of the data to Google Maps. I know there are many articles and data about Salesforce.com and Google Maps already published, but I believe this one is just a little bit different. It is different because one challenge became immediately apparent. In many cases the customer owns several products, and if multiple products are at the same location they are stacked and only one is visible on Google Maps. For those cases I had to merge the information together and display the course information as a single location.

As with any software development project, there is always another way to write the code or implement the design. Sometimes better, sometimes just different. If you see something you do not understand or have an alternate suggestion please speak your mind in a respectful was so all readers can learn from your feedback. For the full code use this GitHub Link
  1. Custom Controller
  2. JavaScript Libraries Required
  3. Google Maps Initilization Fundamentals
  4. Querying the Salesforce Custom Controller
  5. Creating an Array of the Salesforce Results
  6. Determine if there are Multiple Records for a Single Location
  7. Writing the Event Handlers
  8. Creating an InfoBox to Provide the Data on mouseOver Event Listeners
  9. Adding Custom Graphics to the Google Maps Page

Custom Controller
The custom controller is a very basic piece of code with a single simple query. In this case I am just looking for a status of 'open', and for a value in the latitude field on the associated Account record. The code is old enough where I was able to add the test code to the same class, which I think is fine for such a small class file.
custom class customMapController {
 @RemoteAction
   custom static List getOpenCourses(){
    return [SELECT 
           Id, 
           Name, 
           Account__r.Name, 
           Account__r.Location__Latitude__s, 
           Account__r.Location__Longitude__s,
           Account__r.ShippingStreet, 
           Account__r.ShippingCity, 
           Account__r.ShippingState, 
           Account__r.ShippingPostalCode, 
           Account__r.ShippingCountry, 
           Account__r.Website,
           Model__c, 
           Year_Built__c, 
           Status__c 
           FROM Custom_Object__c 
           WHERE Status__c='Open' and Account__r.Location__Latitude__s != null];                    
   }    

   @isTest
   private static void testGetOpenCourses(){
    List control = new List();
    Account account = new Account(Name='Customer One', Location__Latitude__s=36.3809047);
    insert account;
    control.add(new Custom_Object__c(Name='Course1', Status__c='Open', Account__c = account.id));
    control.add(new Custom_Object__c(Name='Course2', Status__c='Open', Account__c = account.id));
    insert control;
    List actual = customMapController.getOpenCourses();
    System.assert(actual.size()==2);
   } 
}
JavaScript Libraries Required
For this project I decided to reference all libraries from the Google CDN instead of using the Salesforce Static Resource. I believe each can be appropriate and do not want to diverge off into the pros and cons o both. Also I included the style tag in the header. Normally I would create and reference a .css file, but with the small amount I decided to simply add it to the HTML header. To perfect it to Marekting satisfaction I used a different Google font to display the product information.

<apex:page controller="customController" showHeader="false"  standardStylesheets="false">
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
    <link href='https://fonts.googleapis.com/css?family=Tenor+Sans|Open+Sans+Condensed:300' rel='stylesheet' type='text/css' />
    <style type="text/css">
      html { height: 100% }
      body { height: 100%; margin: 0; padding: 0;}
      #map_canvas { 
                height: 100%;
                width:100%; 
                }               
      .info {
        font:12px optima, arial,sans-serif;
        border-style:solid;
        border-color:#faf4cb;
        border-spacing:20px 20px;
        border-radius:10px;
        background-color:#faf4cb; 
        line-height: 1.5;
        letter-spacing:1px; 
     }  
      </style>          
    <script type="text/javascript"  src="https://maps.googleapis.com/maps/api/js?sensor=false"></script>
    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>  
    <script type="text/javascript" src="https://google-maps-utility-library-v3.googlecode.com/svn/tags/infobox/1.1.9/src/infobox_packed.js"></script>  

Google Map Initialization
As the jQuery .ready() event is fired there are a few initialization steps that occur with the Google Map. This is standard for all Google Maps created and nothing special for this code example. However you should take note of the global variables about the .ready() event as these were required in the effective use of the InfoBox discussed later in this article.
  var map;      
        var ib = new InfoBox();
        
          //initialize to create the base map  
        $(document).ready(function() {
            function initialize() {    
                var courseList=[];
                var processedCourses=[];
                var myLatLng = new google.maps.LatLng(40.707123,  -99.033065);
                  
                //setting map options
                var mapOptions = {
                    zoom: 4,
                    center: myLatLng,
                    mapTypeId: google.maps.MapTypeId.ROADMAP
                  };
                
                    //adding the map to the div tag                    
                 map = new google.maps.Map(document.getElementById('map_canvas'), mapOptions);  
Querying the Salesforce Custom Controller
I created the custom controller to move the details to the backend.  I probably could of written the query directly on the Visualforce page, but chose not to.
var address = customController.getOpenCourses(function(records, e){ 
   //go through reach record returned and create a list of course records in which further action such as merging records is taken in subsequent steps.  
  
Creating an Array of the Saleforce Results
Putting the results returned from Salesforce into a list of arrays for easier management in the next steps.
$.each(records,function(index, course){ //each record in the courseList uses the longtitude and latitude as the key for the record. This key will be used to search for duplicate records later. courseList['key'] = course.Account__r.Location__Latitude__s + ':' + course.Account__r.Location__Longitude__s; //add an array of properties for each record to the course list. var c = {}; c['key']=course.Account__r.Location__Latitude__s + ':' + course.Account__r.Location__Longitude__s; c['lat']=course.Account__r.Location__Latitude__s; c['lon']=course.Account__r.Location__Longitude__s; c['customer']=course.Account__r.Name; c['model']=course.Model__c; c['city']=course.Account__r.ShippingCity; c['state']=course.Account__r.ShippingState; c['country']=course.Account__r.ShippingCountry; c['website']=course.Account__r.Website; courseList.push(c); }); //ends .each //now I am going to go through the first list and create a 2nd list merging any duplicates using the longitude and latitude as the location key $.each(courseList, function(key, value) {
Determine if there are Multiple Records for a Single Location
Iterate through the list of records and process each record as a Google Map Marker once. If the location is a
duplicate using the coordinates as the key, merge the information into the first record added as a Marker. The purpose of thee processedCourses list is to determine whether than location has
already been added preventing duplicates. I am sure there are a dozen ways to dice this cat, and this is the way I chose. I encourage you to create your own solutions that might be closer to meeting
your needs if this approach does not work for you.

if( $.inArray(value['key'], processedCourses) == -1 ) { //return all of the course records with the same key. The value of results (one or more records is passed to the event listeners where the information is merged into a single Google Map marker. var result = $.grep(courseList, function(v,i) { return v['key'] === value.key; }); // Create the marker var marker = new google.maps.Marker({ position: new google.maps.LatLng(result[0]['lat'], result[0]['lon']), map: map, title:"", url:"" }); Calls the event handlers functions for each of the marker objects. See this code further along in the article. setMouseOver(result, marker); //create the mouseover event listener setMouseOut(marker); //create the mouseout event listener setMouseClick(marker); processedCourses.push(value.key); }//end if processedCourses == -1 (meaning we have already processed this record) });//end $.each courseList }); //end getOpenCourses call to custom controller

Writing the Event Handlers
The evaluation of the list for duplicates is above, but the actual merging of the data into a single marker occurs in the setMouseOver function. The information from the Salesforce location(s) is formatted to to include trademarks for some, but not all of product models and slightly different display for international locations. The final string is passedto the actual InfoBox that pops up on the Google Maps Marker mouseOver event.

 function setMouseOver(result, marker){
  var model;
  var location;       
 var weburl;
Loop through the records in the results.  The first record is treated different than the rest because some of the information is the same on all records.  The only thing different on the records beyond the first the course model.
  for(var i = 0; i< result.length; i++) {                         
    if(i==0){ 
       customer = result[i]['customer'];  
       city = result[i]['city'];
       state = result[i]['state'];
       country =  result[i]['country'];
       web = result[i]['website'];
        
      if( result[i]['model'].indexOf("Special Model")>= 0){
          model = result[i]['model'].replace("Special Model", "Special Model®");  //adding the trademark symbol for certain models
      }else{
         model = result[i]['model'];
      }
                  
   }else{  
this is not the first record in the results passed into setMouseOver function.  We already have the customer information so all we are going to 
do is collect the model information and append it to the model information we already have for this customer. This is how the merge occurs into a single marker.
if( result[i]['model'].indexOf("Special Model")>= 0){ model = model + ', ' + result[i]['model'].replace("Special Model", "Special Model®"); }else{ model = model + ', ' + result[i]['model']; } }//end if i==0 } //end for results As the locations are international I want to format the address a little bit based on the country. I realize this is incomplete at best, but works enough until I find or write a better solution. city and state if US or CA. For rest the of global city and country if((country !='US')&&(country !='USA')&&(country !='United States') &&(country !='CA')&&(country !='Canada')&&(country !=null)){ location = city + ', ' + country; }else{ location = city + ', ' + state; } if the account record contains a url then create the site name (customer) as a hyperlink. if(web==null){ weburl = '<b>Site: </b>'+ customer + ''; } else { weburl = '<b>Site: </b><a href="javascript:openPage('%20+" web="">' + customer+ '</a>'; } now that we have all of this information it is time to use the infoBox and format the display when a mouseOver event occurs passing in the marker object and using the variable instantiated in this function Creating an InfoBox to Provide the Data on mouseOver Event Listeners
With the text string created for the marker 'boxText' a mouseOver event is created which used an InfoBox to display the text generated for display and a link to the customer website if available in Salesforce. google.maps.event.addListener(marker, "mouseover", function(event) { var boxText = document.createElement("div"); not that I would recommend inline styles, but had issues trying to point to a class so this was the prudent solution. With time I will go back and rehash when I refactor this code. boxText.style.cssText = "font:12px optima;border-style:solid; border-color:#faf4cb; border-spacing:20px 20px; border-padding:20px; border-radius:10px; background-color:#faf4cb;"; boxText.innerHTML ='<div class="info">'+ weburl + '<b>Location: </b>'+ location + '<br />'+'<b>Course(s): </b>' + model +'<br />' +'</div>'; var myOptions = { content: boxText, disableAutoPan: false, maxWidth: 0, pixelOffset: new google.maps.Size(-140, 0), zIndex: null, boxStyle: {opacity:1 ,width: "280px"}, closeBoxMargin: "10px 2px 2px 2px", closeBoxURL: "", infoBoxClearance: new google.maps.Size(1, 1), isHidden: false, pane: "floatPane", enableEventPropagation: false }; Create the InfoBox using the options set above. The text formatted about from the results passed in is displayed in the InfoBox ib = new InfoBox(myOptions); if (ib) {ib.open(map, this);} }); //close mouseover listener }//close function setMouseOver. Go to the next result in the list of courses queried from Salesforce. This course is surprisingly quick, but performance would be an issue if you have several thousand locations. Currently the list is use is about 300 records.

In the code about where we are looping through all of the customer courses you will notice there are calls to three functions at the end of the for each loop. The second function called is setMouseOut. What happens here is the InfoBox stays open for 1.6 seconds so the user has an opportunity to click on the website URL in the InfoBox after the mouse leaves the marker. This was a little tricky to understand when I first wrote the code. In my solution I moved the variable ib to a global scope at the beginning of the code.
 function setMouseOut(marker){
   google.maps.event.addListener(marker, "mouseout", function(event) {                           
   //create ib2 otherwise scrolling across multiple markers causes loss of focus on infobox object
   var ib2 = new InfoBox();
   ib2 = ib;
   setTimeout(function(){
    ib2.close(); },1600);
   }); //close mouseout listener
  }//close function setMouseOut

The setMouseEventClick function is used to zoom into the area where the marker was clicked. While the user could click on the Google Map to zoom in this is a convenient way to take a closer look at the location of a marker. Keep in mind this is different than the click of the website URL on the InfoBox
 function setMouseClick(marker){     
  google.maps.event.addListener(marker, "click", function(event) {
  map.setZoom(9);
  map.setCenter(marker.getPosition());
  });//close click listener
}//close function setMouseClick

Adding custom Graphics to the Google Maps Page
Adding our own or company logos and possible styling personalizes the page making it clear what the locations represent. With a little additional work you can control the location of the page where the logo occurs and add a legend(as a graphic) for for further explanation in the case of different color Markers.
function addLogoControl(controlDiv) {
     controlDiv.style.padding = '5px';
     var logo = document.createElement('IMG');
     logo.src = "{!URLFOR($Resource.Logo)}"
     logo.style.cursor = 'pointer';
     controlDiv.appendChild(logo);

     google.maps.event.addDomListener(logo, 'click', function() {
       window.location = 'http://companywebsite.com'; 
     });

}
 function openPage(url){
                window.open(url,'_blank');      
        }   

The last section of code on the visualforce page is the basic html body that contains the map and was assigned at the beginning of the jQuery ready event. This is pretty standard for Google Maps pages.
<body>
     <div id="map_canvas"></div>      
</body>
</apex:page>

As a reminder this code is all contained within a Salesforce visualforce page using a remote Javascript call to a custom controller for the locations and data to be displayed. Displaying the locations is fairly easy. I hope this example helps you understand ways to manipulate the list returned (in this case merge of records in the same location). and the use of the InfoBox to display the information. Personally I think the InfoBox is great and looks so much better than the other standard options on displaying Google Marker information.

Friday, September 19, 2014

Salesforce Mobile Single Page Application using the Force.com-JavaScript-REST-Toolkit

After many years of developing and customizing CRM Applications, I have now added Salesforce.com to my repertoire. In the nearly 3 years since, I have found Salesforce.com to be a flexible platform for solving problems by simply extending functionality on the existing model. As an example for this post I have a custom application within Salesforce which is used to help manage custom steel fabrication jobs, some weighing in excess of 100,000 pounds with hundreds and thousands of parts. Just imagine a giant erector set. The pain point experienced was in the packing of these jobs for shipment (shipped over 5 continents), things invariably got missed for a variety of reasons.

A Bill of Materials (BOM) for the job already existed in our custom Salesforce.com application, but shipping was relegated to a paper version, sometimes many pages long. The solution was to extend a small portion of the Salesforce.com custom application to shipping to enable tracking of exactly what was and was not loaded (and for management to review progress) for shipment. After some thought and research I decided to create a small Single Page Application (SPA) using a visualforce page with the Force.com JavaScript Rest Toolkit. Now for those of you saying oh I would of never done it that way, the good thing about Salesforce.com is that there are many ways. It was also an opportunity to learn the Force.com JavaScript Rest Toolkit and more about SPAs.

A key requirement was usability and performance as there are a many parts to load, and time is always of the essence. In keeping it simple the solution has just three screens:
  1. List of Open Jobs

  2. List of Parts for Selected Job
    • Find the Part number by scrolling.
    • Find the Part number by entering the first few characters in the filter.

  3. Mark as Loaded for Selected Part
    • Leave the default full quantity or override with a partial quantity.

  4. Repeating Screen Two once the first part in the list has been loaded.
    • Note the first Part is no longer in the list as the default full quantity was used.
In the end this is not exactly rocket science, but the implementation of this simple solutions helps manage a big problem. The mobile application is very quick, the user can use quickly filter down the long list of parts, and when the full quantity of the part has been marked as loaded the part disappears from the list. In the end the Parts list for a Job will be empty when the Parts are all loaded. Anything remaining in the list identifies something not accounted for in the load.

Enough about generalities, let's get down to the details and some code. To see the code in full follow this link to GitHub. The first step is to setup the Force.com JavaScript Rest Toolkit. It is available on here on Force.com-JavaScript-REST-Toolkit on GitHub. The setup (which is well explained in the GitHub repository) process requires a few steps to include:
  1. Add the forceTK component to Salesforce.com
  2. Add the forceTK Controller to Salesforce.com
  3. Add the forceTKControllerTest to Salesforce.com
The next thing we need is the Mobile Pack for jQuery which has all of the libraries and stylesheets required for the SPA. The jQuery Mobile pack is also available on GitHub. The code in this SPA is similar to the sample provided with the jQuery Mobile pack on Git Hub, but will extended functionality to include navigating to different menu levels. In this article I am going to reference and talk about key points in the code. For the full code detail use this GitHub link. To layout the flow of the SPA I will use some pseudo code first to demonstrate the flow of the logic.
  1. Instantiate the ForceTK Client
  2. Create objects for a Job, Part, and PartDetail
  3. Create click handler for button on PartDetail Page
  4. Query and Display List of Jobs
  5. Click and Job and Display Parts for the Job
  6. Click on Part and Display Part Detail
    • Which only has quantity to load
  7. Click on Mark as Loaded Button
    • Change part quantity if not all loaded.
  8. Redisplay the Parts for the Job
    • The part quantity change reflected if not all were loaded
    • The part no longer in the list if all were loaded

Initialize the ForceTK Client
While it is not readily apparent what the initialization process does, it is a step that you cannot skip.
 
    var $j = jQuery.noConflict(); 
    var client = new remotetk.Client();
    Force.init(null,null,client,null);

Create objects for a Job, Part, and PartDetail
Notice the Detail object is a little different as we are posting a this object record back to the Salesforce.com custom app when marking an item as loaded. The calculation of the remaining quantity to be loaded is based on the quantity on the Part (from the BOM) less the roll-up field of the child records for each time this part was loaded.
 

    var Jobs = new SObjectData();
    Jobs.errorHandler = displayError;

    var Parts = new SObjectData();
    Parts.errorHandler = displayError;

    var Details = new SObjectData('Job_Shipping_Details__c',['Id','Job_Part__c','Number_Loaded__c']);
    Details.errorHandler = displayError;

Create click handler for button on PartDetail Page
Using the jQuery ready event for the page, the click handler is created for the Save button (Label: Mark as Loaded).The click handler simply calls the updatePartRecord(event), which creates a child record to the parent Part in the custom Salesforce app.
$j(document).ready(function() {
   regBtnClickHandlers();
   getOpenJobs();
 });

  function regBtnClickHandlers() {                        
    $j('#save').click(function(e) {
     updatePartRecord(e);
    });
 }
Query and Display List of Jobs
Using the jQuery ready event for the page, the list of Jobs is loaded to the page using a simple query.
$j(document).ready(function() {
   regBtnClickHandlers();
   getOpenJobs();
 });

 function getOpenJobs() {
    Jobs.fetch('soql',"SELECT id, Name, Status__c FROM Job__c WHERE Status__c ='Open'",function() {
       showJobs(Jobs.data());
    });
 }
As we show the Jobs the code gets a little more complex as a link is added to query all of the parts for the selected job. This is where the code differentiates from the jQuery Mobile Pack example as this creates navigation through three levels of parent child records within Salesforce.com.
 function showJobs(records) {    
  $j('#cList').empty(); //make sure the list on the page has been cleared
   $j.each(Jobs.data(),
      function() {
        var newLi = $j('<li></li>');     //create a new list       
        var newLink = $j('<a id="' +this.Id+ '" onclick=getAllParts("'+this.Name+'"); data-transition="flip">[Job Name]: ' +this.Name+ '</a>'); //create a link variable
        newLink.click(function(e) {  //assign a link to the variable
          e.preventDefault();
          $j.mobile.showPageLoadingMsg();    //displays a loading image                 
          $j('#jobId').val(Jobs.findRecordById([this.id]).Id);
          $j('#jobName').val(Jobs.findRecordById([this.id]).Name);
          $j('#error').html('');    
        });

        newLi.append(newLink);       //append the link to the list     
        newLi.appendTo('#cList');   //append the list to the html spa list
   });//close each                
   $j.mobile.hidePageLoadingMsg(); //hides the loading image
   $j('#cList').listview('refresh'); //refresh to show all of the changes to the list
}  //close showJobs  
Click and Job and Display Parts for the Job
When the Job is selected, the getAllParts function is called, which loads all of parts for the job that meets the criteria of having a shipping load balance of greater than 0. When the parts lists loads the quantity to be loaded is also displayed along with the Part number.
 
  function getAllParts(name){
   var query = 'SELECT id, Name, Description__c, Shipping_Balance__c,Quantity__c FROM Job_Part__c WHERE Shipping_Balance__c > 0 and Shipping__c = true and Job__r.Name=' +"'"+ name +"'"+ "Order By Name ASC";
   Parts.fetch('soql',query,function() {
     showParts(Parts.data());
   });
 }
Once the Parts are queried, the showParts function is called and works much like the showJobs function which creates a list of Parts each with a link to the next level down in the record hierarchy.
 
 function showParts(records) {    
   $j('#pList').empty(); //clear the parts list
    $j.each(Parts.data(), //loop through each of the parts
        function() {
   var newLi = $j('<li></li>');                                
   var newLink = $j('<a id="' +this.Id+ '"data-transition="flip">' +this.Name+ '   :   (' + this.Shipping_Balance__c+')</a>');                     
          newLink.click(function(e) {
     e.preventDefault();
     $j.mobile.showPageLoadingMsg();
     $j('#shippingBalance').val(Parts.findRecordById([this.id]).Shipping_Balance__c);
     $j('#partId').val(Parts.findRecordById([this.id]).Id);
     $j('#partName').val(Parts.findRecordById([this.id]).Name);
     $j('#error').html('');                                          
     $j.mobile.changePage('#dialogpage', {changeHash: true});
          }); //end of the new link functionality
   
          newLi.append(newLink);            
   newLi.appendTo('#pList');
     }); //close each
            
     $j.mobile.hidePageLoadingMsg();
     $j('#partsHeader').text('Shipping Parts for ' + $j('#jobName').val());
     $j.mobile.changePage('#partspage',{changeHash:true});
     $j('#pList').listview('refresh');
  }     
Click on Part and Display Part Detail
In this instance a Part Detail record is written back to Salesforce.com, and new calculated is run on Salesforce (simply using the rollup field) and if there is a balance of greater than 0 to be loaded by shipping the Part remains in the list with a new quantity. If the remaining quantity to be loaded is 0 then the Part is no longer in the list.
 
  function updatePartRecord(e){
   e.preventDefault();
   var pId = $j('#partId').val();
   var sCount = $j('#shippingBalance').val(); get the quantity loaded from the 1 field form presented to the user.
   var record = Details.create('Job_Shipping_Details__c',{'Job_Part__c':pId,'Number_Loaded__c':sCount});
   Details.sync(record, successCallback); //with record successfully synced back to Salesforce the successCallback is called to load the list of Parts reflective of the change just made.
 }


Redisplay the Parts for the Job With record successfully synced back to Salesforce the successCallback is called to load the list of Parts reflective of the change just made.
 function successCallback(r){
  getAllParts($j('#jobName').val()); //make another call to getAllParts to reload the Parts list
  $j.mobile.changePage('#partspage', {changeHash: true});
 }

The SPA HTML

As a SPA all of the HTML is contained within a single page.  Throughout the code every reference to $j.mobile.changePage uses a portion of the code below rendering the three available pages, 'listpage', 'partspage', and 'dialogpage'.  The jQuery mobile and css magic takes over and does the rest with little effort of the part of the developer.  What a great library!

<body>   
    <div data-role="page" data-theme="b" id="listpage">                
      <div data-role="header" data-position="fixed">
        <h2>Jobs</h2>
      </div>
      <div data-role="content" id="jobList">            
        <ul id="cList" data-filter="true" data-inset="true" data-role="listview"  data-theme="c" data-dividertheme="b"></ul>
      </div>
    </div>
    <!--============== Parts Page ================= -->
    <div data-role="page" data-theme="b" id="partspage">                
      <div data-role="header" data-position="fixed">
        <h2 id="partsHeader"></h2>
        <a href='#listpage' id="add" class='ui-btn-right' data-icon='back' data-theme="b">Back</a>
    </div>
       <div data-role="content" id="partList">            
         <ul id="pList" data-filter="true" data-inset="true" data-role="listview"  data-theme="c" data-dividertheme="b"></ul>
      </div>
    </div>
    <!--============== Dialogbox ================= -->
 <div data-role="dialog" data-theme="b" id="dialogpage">
      <div data-role="header" data-position="fixed">
        <a href='#partspage' id="back2JobsList" class='ui-btn-right' data-icon='arrow-l' data-direction="reverse" data-transition="flip">Cancel</a>
        <h1>Shipping Load</h1>
      </div>
      <div data-role="content">
      <div data-role="fieldcontain">
     <label for="shippingBalance">Balance to be Loaded:</label>
     <input type="number" name="shippingBalance" id="shippingBalance" />
   </div>
   <h2 style="color:red" id="error"></h2><br/>
   <input type="hidden" id="jobId" />
         <input type="hidden" id="partId" />
         <input type="hidden" id="jobName" />
         <input type="hidden" id="partName" />
   <button id="save" data-role="button" data-icon="check" data-inline="true" data-theme="b" class="save">Mark As Loaded</button>
   </div>    
    </div>  
 </body>

In Conclusion

My objective in this blog was to provide an example of how I used a simple SPA, to extend an existing custom Salesforce.com app to solve a problem.  Hopefully this example will help you understand and create solutions of your own.