Oh my god. It's full of code!

Visualforce PageBlockTable save as you work

Recently I was working on a visualforce application that is intended to be run out in the field by people on laptops with spotty internet connections. These connections are slow, and we have had problems with save requests actually not working due to their size and the instability of the internet connection. This application is basically a large list view created in a Visualforce pageBlockTable.

I scratched my head for a while trying to think of a way to deal with this issue. It occurred to me, that if the application saved a users work row by row it would be less likely to suffer from the issues described above. Instead of passing the whole list to save, just pass one row at a time, and give the user confirmation it saved. This was a bit of a challenging prospect at first, but with Javascript remoting fresh in my mind, I came up with a plan.

1) On the apex:inputField in my pageBlockTable, attach an onChange javascript event handler. Call it ‘saveRow’. Pass in the ID of the current row so the function can get the rest of the values from that row.

2) create the saveRow javascript function that takes the row index as a parameter. Use that row number and some more javascript to pull the rest of the field values from that row in the pageBlockTable. Then use Apex/Javascript remoting to call an Apex class to save the data. Then print the result back to the user.

3) create the remote save method in my Apex controller that takes the record Id and other parameters we want to update. Create an object of the required type, use the passed in ID to id it, set the other values and save it. Return information to the calling javascript function to display to the user.

So my first challenge was how to find the row ID of current row in my pageBlockTable so it could be passed to the javascript, which would then use that to read data from the other fields it needs to save. You can find the ID of the current row by counting the number of table rows before it. Also, I wanted to pass along the ID of this record (one less thing to have to read from the table later) I did it like this.

<apex:inputField value="{!payment.Status__c}" id="checkIn" onChange="saveRow(jQuery(this).closest('tr').prevAll('tr').length, '{!payment.id}')">

So now when that dropdown list is changed it will call the saveRow javascript function with it’s row ID and the id of that record (this list was of payment records. Hence payment.id is also being passed).

Next I need my javascript function. It ended up looking like this

function saveRow( rowNumber, paymentId)
{
    jQuery('.loader').show();
    var thisStatus = jQuery("#page\\:fieldBlock\\:form\\:fieldTable\\:"+rowNumber+"\\:checkIn").val();
    var thisRespNo = jQuery("#page\\:fieldBlock\\:form\\:fieldTable\\:"+rowNumber+"\\:respNum").val();
     
    var message = '';
    try
    {
        hostAppController.saveRow( paymentId, thisStatus, thisRespNo, function(result, event)
        {
            if(event.status)
            {
                 if(result.success == true || result.success == 'true')
                 {
                     message = "Save Status: <font color='green'>"+result.data+"</font>";
                 }
                 else
                 {
                      message = "Save Status: <font color='red'><b>"+result.data+"</b></font>";
                 }
                 jQuery('#saveRecordResult').html(message);
                 jQuery('.loader').hide();
                 jQuery('#fieldFilter').val('');                 
            }
        }, {escape:true});
    }
    catch(e)
    {
        jQuery('#saveRecordResult').html('Error' + e);
        jQuery('.loader').hide();
        jQuery('#fieldFilter').val('');    
    }
}

You can see how the rowId is used to read the other values we want from the table. Using jQuery it was easy to extract the extra values. We then make a call to the hostAppController class and the saveRow method within it. We pass in the paymentId, the status, and the respondent number. Then evaluate the results of the call and display it (I also manually show and hide my animated loading bar gif to let the user know something is happening).

So now we need the apex class that handled actually saving the data. It’s a simple little thing.

    global class remoteObject
    {
        public boolean success{get;set;}
        public string message{get;set;}
        public string data{get;set;} 
    } 
        
    @RemoteAction
    global static remoteObject saveRow(string Id, string status, string respondentNumber)
    {
        remoteObject returnObj = new remoteObject();
        returnObj.success = true;
        returnObj.message = 'Record Saved';
        returnObj.data = 'Payment '+Id+' saved with status '+status+' and respondent number '+respondentNumber;
                
        try
        {
            Payments__c thisPayment = new Payments__c(Id = id);                               
            thisPayment.Status__c = status;
            thisPayment.Respondent_Number__c = decimal.valueOf(respondentNumber);
            update thisPayment;
        }
        catch(exception e)
        {
            returnObj.success = false;
            returnObj.message = 'Error saving record ' + e.getMessage();
            returnObj.data = 'Error ' + e.getMessage() + ' ' +e.getCause() + ' ' +e.getTypeName();
        }
        
        return returnObj;
    }

You can see I created a custom class called remoteObject. I created this just for returning to javascript functions. It’s easy to evaluate the result of the call with the success property, and the data and message properties can contain extra data to give to the user. You could of course pass back whatever you like. A string, a JSON object, some XML, whatever you want. This method is nice though because this custom class gets handled just like a native javascript object so you can access it’s key value pairs without any fancy parsing or anything.

And there you have it. With three easy steps you now have a system that can save as you work. It handles errors gracefully and presents the user with confirmation that the save actually happened. Hope this helps someone out there!

4 responses

  1. ken

    Daniel, this is pretty straightforward and elegant. At first I thought it was a similar scenario to one we just solved with a bunch of new, uninserted rows.

    This will come in handy. I will let you know if we hack it into anything new.

    –k

    June 24, 2011 at 10:40 pm

    • Thank you for the compliment, I am glad you like it! I didn’t know if this was a ‘best practice’ type of implementation, but overall it seems to be working really well and I am pleased with the performance. The javascript remoting is really fast! Certainly keep me posted of anything cool you do with it.

      June 29, 2011 at 4:49 am

  2. Daniel – this is very useful. I’m using this as template/blue print to implement a Campaign Console where a user can quickly change the status and add a note for a specific Contact or Lead included in a campaign.

    The difference is that I’m invoking the saveRow() JS function w/ a commandButton onclick() event.

    When you were troubleshooting, how did you determine if the issue was on the server side (Apex class) or in the jQuery? I’m not able to track down if my issue is in how I’m using the jQuery or in my Apex code.

    Thanks in advance! This is the only thing on the web that has this type of solution.

    September 27, 2012 at 7:23 am

  3. FYI / update – using the $Component global variable makes it much easier to get the component ID, don’t need to figure out the row index and wrestle query path.

    http://goo.gl/y1ODf

    Cheers!

    September 27, 2012 at 9:37 pm

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