jQuery – Cloudstock 2012 – Part 1


Hello one and all!  I was honored to have been asked to present a Developer Commons session on jQuery at Cloudstock 2012.  If you were in attendance during my presentation or just stumbled upon my blog – welcome one, welcome all.

My post here is to run through my presentation – sit back, grab a beverage, and here we go.  Here’s part 1 – intro and example 1:  table column sorting.

Why is it important to use technologies like jQuery?

jQuery is a very important tool in your kit for one main reason:  User Interaction.  You probably can code the best thing in the universe, but if your users don’t see it as easy and fun to use, you’re sunk.  Your users hold the key to overall solution adoption.  jQuery can help you!

Developers tend to think just like developers, just as users tend to think like users; VERY seldom do you ever get a hybrid situation.  As a developer, you need to leverage your users to head off adoption issues before you present them with “version 1” – as you can never redo a first impression.

Take this business case – you are in a beautifully-architected solution that manages all kinds of information, but a user asks you “how do I add an account to Salesforce?”  Relying on the good Salesforce mantra of declarative vs. programmatic, your initial reaction would be to tell them to hit the “Accounts” tab in their tab bar, hit “New” and enter the new information – then head back in your solution.

To a developer – this sounds perfectly fine.  Why should I have to rebuild something that already exists?  That’s not smart!  What if x, y, or z changes – I have to go in and change my custom (basic) functionality!  We (developers) also tend to think in totalities – “A user wants to add an account” means “they want the entire flipping screen available to them!”.  Potentially not true!

Deploying to users – breaking their stream of thought and level of engagement in your solution is a total deal-breaker.  Your dream of 100% enthusiastic “ticker-tape parade in the streets” adoption rate just will not happen.  They’ll use it, but they’ll grumble and put post-it notes on their screen to go back and enter those new accounts after they’ve done what they need to do in the solution.

jQuery can help!!  Ask your user – “what do you really need to track on a new account while working in the solution?”  They may come back and just need to enter BASIC information, possibly with a workflow Task reminding them to go back later and fill everything else out – that’s very doable with LITTLE code.

Too little?  Too much?  Finding that “Goldilocks Zone”

I know you’ve seen it.  That gawdy page on the Internet (think back to the old Frontpage frames days perhaps?) that has every single possible plugin and animated GIF thrown in there like someone sneezed on the window?  Yeah, I know you’ve all seen it.  Just as much as you’ve seen the solution where you think, “c’mon dude, you did THAT…why not just take it a LITTLE farther??”.

There is a FINE line between too little and too much.  It’s ultimately up to experience and having some intuition (as well as talking to your users!!!) to accurately gauge it.  This post is going to go through some pretty low-key, good return scenarios where you can add a little bit of sex appeal to your solution.

Salesforce is an incredibly deep and capable platform.  Do NOT replace the calendar control that they provide unless it’s ABSOLUTELY WITHOUT A DOUBT a do-or-die success factor of your project.  Even then – I’d still fight it. Consistency is just as important as functionality…even at the expense of the VP not being able to press a special key combination to automatically choose a date.

Still having doubts?  ASK YOUR PEERS!  ASK THE COMMUNITY!  Salesforce (and jQuery’s community) have great resources for you – just waiting to be leveraged!  Hit up the #askForce hashtag on Twitter or the Force.com Developer Boards.  Both are excellent places to get answers from real people.

Example 1:  Table Sorting

You can create a Visualforce page like the best of them.  Some PageBlocks, PageBlockSections, and a PageBlockTable.  Works great.  A user says, “I want to be able to sort the columns!”.  Well crap…that’s not natively available.  Enter jQuery to the rescue!  Using a jQuery plugin called “tablesorter”, we can either create a native HTML table and bind it to that, or through one more line of Javascript / jQuery code – bind it directly to the PageBlockTable.  The same goes for just about any native Visualforce widget – you can bind just about anything to them.

Some quick code:

1.  You need to pull in your jQuery, jQueryUI, jQuery CSS, and Tablesorter plug-in.  I packaged them all in a ZIP file that I stuck into Static Resources.  Typically I recommend this approach over getting them from Google or another public source – just because I don’t like things changing in my app that I don’t change myself.

<script src=”{!URLFOR($Resource.jQuery,’/js/jquery-1.5.1.min.js’)}” type=”text/javascript” />
<script src=”{!URLFOR($Resource.jQuery,’/js/jquery-ui-1.8.13.custom.min.js’)}” type=”text/javascript” />
<script src=”{!URLFOR($Resource.jQuery,’/tablesorter/jquery.tablesorter.min.js’)}” type=”text/javascript” />

<apex:stylesheet value=”{!URLFOR($Resource.jQuery,’/css/ui-lightness/jquery-ui-1.8.13.custom.css’)}” />
<apex:stylesheet value=”{!URLFOR($Resource.jQuery,’/tablesorter/style.css’)}” />

2.  Creating your jQuery object – always do a NoConflict in Salesforce so you don’t muck up their stuff.

var j$ = jQuery.noConflict();

3.  Either bind the Tablesorter to a native HTML table (first block) or use the JS getElementById to traverse the Salesforce rendering engine to bind to the native controls.

////////////////////////////
// Wait for document.ready
////////////////////////////
j$(document).ready(function(){

////////////////////////////////
// Bind TableSorter to a HTML table filled with controller data
////////////////////////////////
j$(“#tblNativeSortable”).tablesorter({
widgets: [‘zebra’],
headers: {
// Specify any columns that you don’t want to sort by
0: { sorter: false },
}
});

// Use document.getElementById to find VF elements
var elResource = document.getElementById(‘{!$Component.pgJQueryDemo:frmMain:pbMain:pbsStandardList:pbtNative}’);

////////////////////////////////
// Bind TableSorter to the native SF apex:pageBlockTable
////////////////////////////////
j$(elResource).tablesorter({
widgets: [‘zebra’],
headers: {
// Specify any columns that you don’t want to sort by
// 2: { sorter: false },
}
});
});

Here is the VF code behind these tables (there is a controller behind this doing the list GET)

<apex:pageBlockTable id=”pbtNative” value=”{!lstAccounts}” var=”a”>

<apex:column value=”{!a.Id}”/>
<apex:column value=”{!a.Name}”/>
<apex:column value=”{!a.Website}”/>
<apex:column value=”{!a.Phone}”/>

</apex:pageBlockTable>

<table id=”tblNativeSortable” class=”list tablesorter” border=”0″ cellpadding=”0″ cellspacing=”0″>
<thead>
<tr>
<th>Delete?</th>
<th>Account Id</th>
<th>Account Name</th>
<th>Website</th>
<th>Phone</th>
</tr>
</thead>
<tbody>
<apex:repeat value=”{!lstAccounts}” var=”a”>
<tr>
<td>
<a href=”#” onClick=”deleteAcct(‘{!a.Id}’);”>Delete</a>
</td>
<td>{!a.Id}</td>
<td>{!a.Name}</td>
<td>{!a.Website}</td>
<td>{!a.Phone}</td>
</tr>
</apex:repeat>
</tbody>
</table>

Tune in soon for the next blog post – on interstitial loading modal dialogs and modal forms!

24 thoughts on “jQuery – Cloudstock 2012 – Part 1

  1. Can you please post the complete example. As this is my first experience with jQuery in salesforce. A complete example helps me to pick up. Hope you consider my request please.

  2. Pingback: jQuery – Cloudstock 2012 – Part 2 | Andrew Boettcher – Salesforce Technologist

  3. Hi
    I like your post but little confuse could you post entire code in a place and upload the zip file
    so that it will be great help.Since i am looking this functionality from long time

    Thanks
    Raj

  4. Great post.

    I’m doing my best to sort a native apex:pageBlockTable. Everything is working correctly, but the native styles on the header appear to be overwriting the tablesorter styling, and I am not seeing an indication of ascending/descending. Any recommendations?

  5. Hi there! I know this is somewhat off topic but I was wondering if you knew where
    I could find a captcha plugin for my comment form? I’m using the same blog platform as yours and I’m having trouble finding one?
    Thanks a lot!

  6. Hi Andrew,
    I am working with this example above and have a question?
    I have your pdtNative pageBlockTable working with value=”{!listviewoptions}”

    Column sort when the first page is loaded.
    After changing the list view to Example: All Accounts – Columns rerendered no longer sort…
    I would love to email you my code if you want for more information

    Thanks for your help and code examples,

    David G.

  7. Hi Andrew,

    Thanks for making this resource available. This is and your 2012 Dreamforce recording on jQuery are really helpful. I’m new to jQuery and am trying to use tablesorter to modify a mass edit Vf page in the Mass Update Mass Edit app. The page and the page block table load fine, but the columns aren’t sortable. In theory, should tablesorter work with any working page block table, or could there be something within the page block table that needs to be adjusted to get tablesorter to work? My page just has the page block table, not the table element – do I need that as well?

    I’d really appreciate any advice you can send my way.

    Thanks much,
    Sandi

    • ** EDITED – silly WordPress killing my HTML comments! **

      Hello Sandi!

      Thanks for the comment! =)

      Typically what I run into when the columns don’t sort is the application of the style to the table or pageblocktable. You can bind tablesorter right to a pageblocktable – but you have to navigate your DOM to do it.

      For instance, if your page looked like this (pseudocode):
      apex:page id=”pgJQueryDemo”
      apex:form id=”frmMain”
      apex:pageBlock id=”pbMain”
      apex:pageBlockSection id=”pbsStandardList”
      apex:pageBlockTable id=”pbtNative”
      — your columns —

      You would have to have this JS /jQuery code in your page:

      ////////////////////////////////
      // Bind TableSorter to the native SF apex:pageBlockTable
      ////////////////////////////////
      var elResource = document.getElementById(‘{!$Component.pgJQueryDemo:frmMain:pbMain:pbsStandardList:pbtNative}’);
      j$(elResource).tablesorter({
      widgets: [‘zebra’],
      headers: {
      // Specify any columns that you don’t want to sort by
      // 0: { sorter: false },
      }
      });

      Note how the “var elResource” navigates the VF structure to get to the pageblockTable. jQuery can’t natively deal with colons, and naturally that’s how everything renders in VF. =)

      Give that a whirl – if you have issues, you can pop your code up in a private pastebin and we can walk through it!

      Good luck!

      -Andy

  8. Hello tech man,

    kindly help me to achieve following implements in below code,

    1.)Display all alphabets with command links on the account’s table and upon click on any of the letter,account table should display account’s records starting with the selected letter.

    2.)If no account’s records is selected and user clicks on opportunities/contacts check boxs then it should show error message that ‘Choose atleast one account record to display opportunities’ error message.Same is the case with contacts also.

    3.)If no opportunities/contacts records are found for the selected account records, it should not display save opportunities/contacts button.

    I here by providing code,please make modifications in below to achieve above functionalities,you can check this code on your developer account.

    Apex controller
    ============
    public class Acc_con_Opp_Details
    {
    //list of collection of the wrapper class object
    public list actwrap {set;get;}
    //list of collection of Account,contact and opportunity objects
    public list accounts {set;get;}
    public list acts {set;get;}
    public list opts {set;get;}
    public list sopts {set;get;}
    public list cnts {set;get;}
    public list snts {set;get;}
    public boolean oppbox {set;get;}//used as check box for opportunity
    public boolean conbox {set;get;}//used as check box for contact
    public boolean flag1 {set;get;}//used in account page block
    public boolean flag2 {set;get;}//used in contact page block
    public boolean flag3 {set;get;}//used in opportunity page block
    public boolean checkallflag {set;get;}//used for header check box
    public Boolean oppError {set;get;}
    public Boolean conError {set;get;}
    //this variables are used for pagination purpose
    private integer totalRecs = 0;//stores no.of total records
    private integer index = 0;//used for tracking offset
    private integer blockSize =5;//for setting size of page
    private String sortDirection = ‘ASC’;
    private String sortExp = ‘Name’;

    //in this constructor we are setting values to boolean values
    public Acc_con_Opp_Details()
    {
    flag1=true;
    flag2=false;
    flag3=false;
    oppError=false;
    conError=false;
    checkallflag = false;
    totalRecs = [select count() from Account];//returns total no.of account records
    getactwrap();//calling getactwrap method.
    }
    public String sortExpression
    {
    get
    {
    return sortExp;
    }
    set
    {
    //if the column is clicked on then switch between Ascending and Descending modes
    if (value == sortExp)
    sortDirection = (sortDirection == ‘ASC’)? ‘DESC’ : ‘ASC’;
    else
    sortDirection = ‘ASC’;
    sortExp = value;
    }
    }

    public String getSortDirection()
    {
    //if not column is selected
    if (sortExpression == null || sortExpression == ”)
    return ‘ASC’;
    else
    return sortDirection;
    }

    public void setSortDirection(String value)
    {
    sortDirection = value;
    }
    //this method displays first five records
    public void beginning()
    {
    flag2 = false;
    flag3 = false;
    oppbox=false;
    conbox=false;
    checkallflag=false;
    index = 0;
    getactwrap();
    }
    //this method displays prevoius records
    public void previous()
    {
    flag2 = false;
    flag3 = false;
    oppbox=false;
    conbox=false;
    checkallflag=false;
    index = index-blockSize;
    getactwrap();
    }
    //this method displays next records
    public void next()
    {
    flag2 = false;
    flag3 = false;
    oppbox=false;
    conbox=false;
    checkallflag=false;
    index = index+blockSize;
    getactwrap();
    }
    //this method displays last remaining records
    public void end()
    {
    flag2 = false;
    flag3 = false;
    oppbox=false;
    conbox=false;
    checkallflag=false;
    index = totalrecs – math.mod(totalRecs,blockSize);
    getactwrap();
    }
    //this variable is used to enable or disable first and previous buttons
    public boolean prev{get
    {
    if(index == 0)
    return true;
    else
    return false;
    } set;}
    //this variable is used to enable or disable next and last buttons
    public boolean nxt{get
    {
    if((index + blockSize) > totalRecs)
    return true;
    else
    return false;
    } set;}
    //used to display opportunities and contacts w.r.t selected accounts
    public void submit()
    {
    flag1=false;
    acts=new list();
    List acflagList = new List();
    for(accountwrapper aw:actwrap)
    {
    if(aw.acflag){
    acts.add(aw.acc);
    acflagList.add(aw.acflag);
    }
    }
    if(acflagList.isEmpty()) {
    oppbox=false;
    conbox=false;
    flag2=false;
    flag3=false;
    oppError=false;
    conError=false;
    }

    //if we select contact check box,then it displays contacts for selected accounts
    if(conbox)
    {

    snts=[select id,lastName,Department,account.name from contact where accountid IN:acts];
    if(snts.size()>0)
    {
    flag3=true;
    conError=false;
    }
    else
    {
    for(accountwrapper aw : actwrap) {
    if(aw.acflag) {
    flag3=true;
    conError = true;
    }
    //ApexPages.addmessage(new ApexPages.message(ApexPages.severity.WARNING,’contact records are not found for selected Accounts.’));
    }
    }
    } else {
    flag3 = false;
    }
    //if we select opportunity check box,then it displays opportunities for selected accounts
    if(oppbox)
    {
    opts=[select id,name,stageName,leadsource,account.name from opportunity where accountId IN:acts];
    if(opts.size() >0)
    {
    flag2=true;
    oppError=false;
    }
    else
    {
    for(accountwrapper aw : actwrap) {
    if(aw.acflag) {
    flag2=true;
    oppError = true;
    }
    //ApexPages.addmessage(new ApexPages.message(ApexPages.severity.WARNING,’opportunity records are not found for selected Accounts.’));
    }
    }
    } else {
    flag2 = false;
    }
    }
    //it hides contacts and dispalys opportunitites on uncheck of contact check box

    public void Hideandshowopp()
    {

    if(oppbox)
    {
    submit();
    } else {
    flag2 = false;
    }

    }
    // it hides opportunities and dispalys contacts on uncheck of opportunities check box
    public void HideandshowCon()
    {
    if(conbox)
    {
    submit();
    } else {
    flag3 = false;
    }

    }

    public void Hideandshowoppcon() {
    Hideandshowopp();
    Hideandshowcon();
    }

    //This method uses a simple SOQL query to return a List of Accounts
    public void getactwrap() {
    string sortFullExp = sortExpression + ‘ ‘ + sortDirection;
    accounts = Database.Query(‘SELECT id,Name,phone FROM Account order by ‘ + sortFullExp + ‘ LIMIT :blockSize OFFSET :index’);
    actwrap=new list();
    // As each Account is processed we create a new accountwrapper object and add it to the wrapper class(accountwrapper) list.
    for(account a:accounts)
    {
    actwrap.add(new accountwrapper(a));
    }
    }

    // this method uses dml operation to edit the existing opportunities values or to insert new oppt values
    public void saveopps()
    {
    list opps=new list();
    opps.addAll(opts);
    upsert opps;
    }
    //his method uses dml operation to edit the existing contact values or to insert new contact values
    public void savecons()
    {
    cnts = new list();
    cnts.addall(snts);
    upsert cnts;
    }
    //This is wrapper class which is collection of other class instances
    //here wrapper class contains both the standard salesforce object Account and a Boolean value acflag
    public class accountwrapper{
    public account acc{set;get;}
    public boolean acflag{set;get;}
    //In this contructor When we create a new accountwrapper object we pass a Account that is set to the acc property.
    // We also set the boolean value to false
    public accountwrapper(account a){
    acc=a;
    acflag=false;
    }

    }

    }

    vf page
    =====

    function checkallflag(allCheckboxes) {

    var container = allCheckboxes;
    while (container.tagName != “TABLE”) {
    container = container.parentNode;
    }

    var inputs = container.getElementsByTagName(“input”);
    var checked = allCheckboxes.checked;
    for (var i = 0; i < inputs.length; i++) {
    var input = inputs.item(i);
    if (input.type == "checkbox") {
    if (input != allCheckboxes) {
    input.checked = checked;
    }
    }
    }
    CallSubmitMethod();
    }

            
             
            

    <!–Select–>

    {!a.acc.Name}

    Opportunities   

    Contacts   
    <!– –>

    Opportunity Id

    Opportunity Name

    Opportunity Stage

    Opportunity Leadsource

    Contact Id

    Contact Lastname

    Contact Department

Leave a reply to David Gifford Cancel reply