Single Page Survey Application with Visualforce, jQueryMobile and Knockout

Introduction

I’ve written a number of survey applications in the past, some aimed at desktop devices and some aimed at mobile. The common theme for all of these was a round-trip to the server to navigate to the next or previous question, which resulted in a number of abandoned surveys started by people with a less than ideal internet connection.  To improve this, I created a Single Page Application, which delivers all of the pages required by the application to the device in a single response. The application retrieves and updates data from the server via API calls rather than carrying out potentially expensive HTTP round trips to the server. For more details on the architecture and pros and cons of Single / Multi Page applications, see Architecting Performant HTML5 Mobile Applications on Force.com: Part 3.

Data Model

The data model for the survey application is as follows:

Survey App Data Model

The left hand side of the data model is the template survey information – a Survey contains some introductory text and a number of Survey Questions.  This is separated from the response data, as I want to be able to change the questions in a Survey without affecting any Surveys that have already been completed, as that could invalidate answers.

When a Survey is sent to a contact, the Survey and associated questions are cloned into a Response that is identified by a unique, non-predictable code, to stop anyone manufacturing codes and answering surveys that they haven’t been sent.

jQuery Mobile

To style my application, I’m using jQuery Mobile (JQM), mainly because I’ve been using it for over three year now and I’m very familiar with it.

JQM is a user interface framework that provides a mobile look and feel for HTML5 web applications. Its purely concerned with presentation, so doesn’t manage your data or provide a business logic layer. Its touch optimised, which means its designed primarily for touch screen interaction. It supports a wide range of devices ranging from phones and tablets to e-readers and desktops – the latter is particularly useful when developing an application, as it allows you to build and test on your  desktop or laptop, with access to better tools to debug and inspect the application than you would have on a mobile device.

JQM uses progressive enhancement to enhance HTML content, providing the best user experience possible for a given device.  Progressive enhancement takes a layered approach to rendering the page:

  1. The first layer is regular HTML, which won’t look particularly great but will be functional.  Older devices and browsers are still able to access the content, but it won’t be as slick as it would be on a modern smartphone.
  2. The second layer is CSS, which enhances the view.
  3. The third and final layer is JavaScript, which adds animation, transitions and ajax requests, among others.

JQM determines the enhancements to be applied via HTML5 custom data attributes,  so defining a div with a data-role of “header”, for example:

    <div data-role="header" data-theme="c">
          <img src="{!URLFOR($Resource.BBLogo)}" class="headerlogo" alt="Bob Buzzard Logo"/>
          <h1>Start</h1>
    </div><!-- /header -->

enhances the div to style it as an application header:

Div styled as applicaiton header

These are not standard HTML attributes, instead they are a way to store additional information on the element which has meaning to the application. In this case, JQM knows that when it encounters the data-role attribute, it needs to apply styling to enhance the element.  If JQM was removed from the application, this attribute would be ignored.

For those who have been using JavaScript frameworks for a while, this is the sort of information that used to be stored in class or rel attributes.

A physical Visualforce page in a JQM application can contain one or more logical web pages, which allows a Single Page Application to be created. JQM naturally lends itself to Single Page Applications when used with Visualforce, as it hijacks page navigation and form submission, resulting in competition rather than cooperation.

Each JQM logical web page is defined as a div with a data-role attribute of page and these are stacked in the physical Visualforce page:

   <div data-role="page" id="p1">
        <div data-role="header">
            <h3>Page 1</h3>
        </div><!-- /header -->
      <div data-role="content">
         Content 1!<br/>
         <a href="#p2">Click here to view Page 2</a>
      </div>
   </div>
   <div data-role="page" id="p2">
        <div data-role="header">
            <h3>Page 2</h3>
        </div><!-- /header -->
      <div data-role="content">
         Content 2!<br/>
         <a href="#p1">Back to Page 1</a>
      </div>
   </div>

In this snippet there are two divs with the data-role of page, which equates to two web pages.  When the application is opened, the web page contained in the first div is displayed, while clicking on the link swaps the content of the first logical web page out of the DOM and swaps the content of the second logical web page in.
jQuery Mobile Multiple Logical Pages

The survey application has the following logical pages:

  • Loading page, which is displayed while the survey information is being retrieved from the server
  • Start page, which displays basic information about the survey and a start button to allow the contact to launch the survey
  • Question page, which displays the question, inputs and any buttons required.  This page is updated as the conact navigates through the survey.
  • Complete page, which is displayed when the contact has completed all questions and the results have been sent back to the server
  • An error page, in case anything goes wrong

Business Logic

The business logic in the survey application must execute client-side, to reduce the round trips to the server, which means JavaScript. The JavaScript is responsible for the following functionality:

  • Retrieving the survey record from the server and checking it is not already complete
  • Redrawing the question page based on the attributes of the current question
  • Sending the contact’s answers back to the server once completed

I’m using JavaScript remoting to interact with the server data, although this could just as well be accomplished via the REST API.

Redrawing the question page requires quite a lot of repetitive DOM manipulation to push values into the page and extract the user’s input, so I’ve chosen to use another framework to reduce the amount of JavaScript that I need to write – Knockout

Knockout

Knockout is a Model-View-ViewModel framework that reduces the need to write code to manipulate the DOM by allowing you to declaratively bind data to an HTML element.  The user interface can then be automatically updated when the value changes, rather than having to locate the element in the DOM and push the new value into it.

The easiest way to understand Knockout is to think of it as as a controller written in JavaScript and present on the page. The View is the HTML markup, the Model is the data used on in the page, contained in JavaScript objects, and the ViewModel is the controller, managing the data and containing the business logic. Knockout is pure JavaScript, so it will work with other frameworks.  Its also very fast – certainly faster than JavaScript that I’ve written to manipulate the DOM!

The key to knockout is the view model – this contains the data being managed and the methods that carry out operations on that data – in effect the JavaScript controller.  A basic viewmodel containing a few of the attributes for this post is as follows:

function postViewModel()
{
    this.subject='Survey Single Page App'
    this.author='Keir Bowden';
    this.libraries='jQuery Mobile, Knockout.js';
}

DOM elements can bind to the ViewModel properties using custom data-bind attributes:

      <h1>Blog Post Details</h1>
      <p>Subject: <span data-bind="text: subject"></span></p>
      <p>Author: <span data-bind="text: author"></span></p>
      <p>Libraries: <span data-bind="text: libraries"></span></p>

Activating Knockout is achieved by creating an instance of the ViewModel and passing this as a parameter to the applyBindings method:

ko.applyBindings(new postViewModel());

When the applyBindings method is executed, the contents of the <span> elements with data-bind attributes are populated with the contents of the referenced property from the ViewModel:

HTML Bound to ViewModel

The bindings demonstrated above are static, where the power of Knockout becomes apparent is through Observables – these are two-way, dynamic bindings that notify subscribers about changes.  Binding to an Observable means that if the value of the property is changed in the ViewModel, the DOM will automatically update to reflect the change, and user input is automatically captured in the ViewModel property.

In my application, I have a ViewModel Observable named ‘current_question’, which contains the metadata about the question to present to the contact and their response. The following markup conditionally renders a textarea form element based on the type of the ‘current_question’ Observable, and binds the response for the question to the textarea, so that anything the contact enters into the textarea will automatically be written to the response.

<div id="textarea" data-bind="visible: $root.current_question().type=='Text Area'">
    <textarea data-bind="value: $root.current_question().response"/> 
</div>

If you’d like to learn more about Knockout, there’s an excellent set of interactive tutorials.

Loading the Survey

In order for a contact to complete a survey, they are sent a link to the survey SPA, which has a URL parameter containing the unique code. When the contact opens the link, the loading logical web page is displayed and the Survey Response record (plus associated Question Responses) is retrieved via JavaScript Remoting for Visualforce.  JavaScript Remoting methods operate asynchronously, so the loading page displays a spinner until the response has been received and processed.

JavaScript remoting consists of three parts: a method in the page’s Apex controller with the @RemoteAction annotation:

/* 
* Remote method to retrieve a survey response and questions based on the survey response code
*/
@RemoteAction
public static Survey_Response__c GetSurveyResponse(String code)
{ 
    Survey_Response__c result=null;

    // query the response and question data for the supplied cod
    List<Survey_Response__c> responses=[select id, Name, Description__c, Code__c, Complete__c, Complete_Date_Time__c, 
                                        Start_Date_Time__c, Survey_Name__c,
                                        (select id, Question_Text__c, Question_Type__c, Index__c, Response__c,
                                         Option_1__c, Option_2__c, Option_3__c, Option_4__c, Option_5__c, 
                                         Option_6__c, Option_7__c, Option_8__c, Option_9__c, Option_10__c,
                                         Help_Text__c, Survey_Response__c
                                         from Survey_Question_Responses__r
                                         order by Index__c ASC)
                                         from Survey_Response__c
                                         where Code__c=:code];
    System.debug('### Responses.size = ' + responses.size());
    // there should be exactly one match - if this is not the case throw an exception
    if (responses.size()!=1)
    {
        throw new SurveyException('No matching survey found');
    }
    else
    {
        // mark the start time for the survey as now
        responses[0].Start_Date_Time__c=System.now();
        update responses[0];
        // set the result to the single matching survey response
        result=responses[0];
    }
    return result;
}

This is invoked from the application via a function in the ViewModel

self.loadSurveyResponse=function()
{
    // execute JavaScript remoting function
    SurveyAppController.GetSurveyResponse('{!$CurrentPage.parameters.code}', self.responseCB, {escape: true});
}

The results are sent to the supplied callback parameter – self.responseCB in this case. This creates a SurveyResponse instance in the ViewModel, a collection of Observable SurveyQuestionResponses and sets the value of the current_question property to the first question in the collection:

// callback for the JavaScript remoting call
self.responseCB=function(record, event)
{
    if ( (!event) || (event.status) ) 
    {
        // check if the user has already completed the survey
        if (record.Complete__c)
        {
            self.completeMsg('You have already completed this survey.');
            $.mobile.changePage('#completepage');
        } 
        else
        {
           // create new JS response object
           var resp=new SurveyResponse(record.Id, record.Description__c, 
           record.Complete__c, record.Complete_Date__c);
           self.survey_response(resp);

           // iterate the question responses and create observables
           // uses an anonymous function as this could not possibly
           // be used for any other purpose!
           $.each(record.Survey_Question_Responses__r,
                         function()
                         {
                             var question=new ko.observable(new SurveyQuestionResponse(
                                                   this.Id, this.Question_Text__c, this.Question_Type__c, 
                                                   this.Index__c, this.Response__c, this.Option_1__c,
                                                   this.Option_2__c,this.Option_3__c,this.Option_4__c,
                                                   this.Option_5__c,this.Option_6__c,this.Option_7__c,
                                                   this.Option_8__c,this.Option_9__c,this.Option_10__c,
                                                   this.Help_Text__c, this.Survey_Response__c)); 

                             self.questions.push(question);

                             // if the current question is empty, set it to this question 
                             if (typeof self.current_question().type === 'undefined')
                             {
                                 self.current_question(question());
                             }
                         }); 

            // setup whether to display the next button 
            if (self.questions().length>1)
            {
                self.hasNext(true);
            }

            // add the input markup
            self.addInput();
            // send the user to the starting page
            $.mobile.changePage('#startpage');
        } 
        $.mobile.hidePageLoadingMsg();
    }
    else if (event.type === 'exception')
    {
        self.error(event.message);
    }
}

Saving the Survey

Once the contact has provided answers to all questions in the survey, the results are written back to the server, again via JavaScript Remoting.

The controller method receives a collection of the Survey Question Response records – as the user cannot alter the Survey Response record itself, this does not need to be sent to the controller.  Each Survey Question Response record contains the ID of the parent Survey Response, which is used to mark the Survey Response as complete:

@RemoteAction
public static void SaveSurveyResponse(List<Survey_Question_Response__c> qrs)
{
    update qrs;
    Id qrId=qrs[0].Id;
    Survey_Question_Response__c qr=[select id, Survey_Response__c from Survey_Question_Response__c where id=:qrId];
    Survey_Response__c sr=new Survey_Response__c(id=qr.Survey_Response__c,
                                                 Complete__c=true,
                                                 Complete_Date_Time__c=System.now());

    update sr;
}

The remote method is invoked from the ViewModel when the contact completes the survey, after the JavaScript SurveyQuestionResponse records are converted into their Force.com equivalents:

// function executed when the user clicks the complete button
self.complete=function()
{
    $.mobile.loading( 'show', { theme: "a", text: "completing", textVisible: true });
    var resps=new Array();

    // turn the JavaScript question responses into force.com equivalents
    for (var idx=0; idx<self.questions().length; idx++)
    {
        var resp=new Survey_Question_Response__c();
        resp.Id=self.questions()[idx]().id;

        if (self.questions()[idx]().type=='Checkbox')
        {
            // find all of the checked elements and set the value into the response, separared by semicolons
            var qr='';
            for (var idx1=0; idx1<self.questions()[idx]().checked().length; idx1++)
            {	
                if (self.questions()[idx]().checked()[idx1])
                {
                    qr+=';' + self.questions()[idx]().options()[idx1];
                }
            }
            if (qr.length>0)
            {
                qr=qr.substr(1);
            }
            resp.Response__c=qr;
        }
        else
        {
            resp.Response__c=self.questions()[idx]().response();
        }
        resps.push(resp);
    }

    // execute JavaScript remoting method to save the response		
    SurveyAppController.SaveSurveyResponse(resps, self.saveCB, {escape: true}); 
}

and the response processed by the supplied callback – self.saveCB:

// callback from the JavaScript remoting save call	
self.saveCB=function(result, event)
{
    if ( (!event) || (event.status) ) 
    {
        self.completeMsg('Thank you for completing the survey.');
        $.mobile.changePage('#completepage');
    }
    else if (event.type === 'exception')
    {
        self.error(event.message);
    }
    $.mobile.hidePageLoadingMsg();
}

Will They Ever Get Along?

Combining JQM and Knockout worked well for the Landing and Start pages, but the Question page threw up a whole new challenge around the form inputs, as JQM carries out its progressive enhancement once, after which Knockout updates the DOM, conditionally showing and hiding elements as it needs to.  This meant that the newly updated and dispayed inputs on the page were styled as default HTML, which was a jarring user experience. Checkboxes, for example, would switch from the JQM styling:

jQuery Mobile Checkboxes

to the default HTML styling:

Default HTML Checkboxes

My first thought was simply to trigger the enhancement of the new elements once Knockout had finished updating the DOM. However, while some enhanced elements can be notified that their contents have changed and that JQM should re-enhance them, this is not supported for all of the elements that I’m using.  A further downside to this approach is that custom Knockout bindings would be required for each element that was dynamically created, which would require a large amount of additional JavaScript to be written.

After a considerable amount of trial and error and digging around on message boards and forums, I was starting to worry that what I was trying to achieve wouldn’t be possible with JQM and Knockout, I finally hit upon a solution – the Knockout appyBindings method can be called multiple times, to allow new markup to be dynamically injected into the page and still bind to the ViewModel. I could therefore replace the input section completely, attach this to the ViewModel via the applyBindings method and then trigger the create event on the input section, which would cause JQM to enhance the new markup.

I therefore had functions in the ViewModel that were executed when the current_question property changed, and replaced the input section with the appropriate form elements for the question, a text area for example:

// output the markup for a textarea
// have to delete and insert as JQM doesn't re-enhance updated DOM elements
self.addTextAreaInput=function()
{
    $('#inputdiv').html(
           '<div id="textarea" data-bind="visible: $root.current_question().type==\'Text Area\'">' +
           '     <textarea data-bind="value: $root.current_question().response"/> ' +
           '</div> ')
    // bind the viewmodel to the markup
    ko.applyBindings(viewModel, $('#textarea')[0]);
    // trigger JQM to enhance the newly created textarea div
    $('#textarea').trigger('create');
}

The full codebase for this application is available in GitHub.

About the Author

Keir Bowden (aka Bob Buzzard) twitter.com/bob_buzzard, is a four-time Force.com MVP and CTO of BrightGen, a Platinum Cloud Alliance partner in the United Kingdom. He holds all 8 Salesforce certifications and is a regular blogger on Apex, Visualforce, and Salesforce1 solutions at The Bob Buzzard Blog, and author of the Visualforce Development Cookbook. 

Published
May 1, 2015
Topics:

Leave your comments...

Single Page Survey Application with Visualforce, jQueryMobile and Knockout