+ Start a Discussion
Kenji775Kenji775 

Filter pageBlockTable as you type

Hey everyone,

I have an application where I need to impliment a filter as you type system. Basically we have a pageBlockTable that loads a few hundered records. My users want to be able to have a text field where they can begin typing the name of a person and have the results in the pageBlockTable filter to match their query. I figure someone else must have already done this and figured I'd ask the community before I try and hammer it out myself. If I were going to do it, I figure the approach would be

 

1) Find all people and populate the pageBlockTable

2) Create text field with onKeyUp event tied to apexActionFunction

3) apexActionFunction sends current value in filter box to an apex method

4) apex method makes a backup of the complete list. Then queries against the original using the parameters provided by the user.

5) return the new filtered list to the pageBlockTable

6) reRender the table.

 

The only part I don't really know how to do is to filter against the list I already have, as opposed to running a whole new SOQL query against the DB (which would be super inefficient). How can I filter a list that exists in the heap instead of running a few query?

paul-lmipaul-lmi
before i answer, i'll ask a question. how many rows will be in the table? Salesforce latency is such that anything more than a handful will really take a nosedive performance-wise as you rerender the table on each keystroke.
Kenji775Kenji775

Between 50 and 300 rows or so. I was a bit worried about performance, but I figured I'd cross that bridge when I come to it.

paul-lmipaul-lmi

ok, so here's the general strategy:

so here's the general strategy. i can't provide sample code because ours is heavily customized and you'd lose a lot in the translation. you're not using the standard controller, you're using a custom one

 

  1. bind an inputtext to a controller getter and setter method for that variable
  2. create a pagereference method that does your query, based on the getter method's return value
  3. add an actionsupport to the inputtext onkeypress/onkeyup that calls your pagereference
  4. add a rerender for the pageblocktable in the actionsupport
  5. add an actionstatus to the page and point teh status attribute of actionsupport at it (actionstatus drives the ajax for some reason).

 

tips:

  • use SOSL instead of SOQL if using larger objects (more than 50K rows in the object)
  • use LIMIT statements in SOQL, and be aggressive with the WHERE clause.
  • use jquery to disable/hide the pageblock while the action is happening, especially if you plan to add links to the table rows. users get frustrated when the content jumps as the platform catches up to their requests. Salesforce can't handle the transation as fast as they can type. Salesforce < Google Instant.
  • if the table is below a full form, and not just a single box, consider using onchange for your actionsupport, especially if you require input on more than a single field. it is more performant because it's a single request (when they leave the inputtext), and there's less content "jump" as salesforce catches up to multiple requests if the user types with any speed faster than a typical grandmother...:)

 

Kenji775Kenji775

Good information, thank you.

 

I havn't done binding before, that basically just means that value in the box will be the value in the variable as apex sees it correct? So I can avoid doing any value passing (as far as the actual search string is concerned) like I normally would have to?

 

Also, you are saying I should just run fresh SOQL queries instead of of somehow trying to query against the full list I already have in memory? If so thats fine, just making sure I understand.

 

Thanks again.

Kenji775Kenji775

I actually decided to scrap the filter as you type. Just a simple search button will work for now. I think I have everything set up, but my filter doesn't actually do anything. It always returns everyone for that campaign. It's like it just doesn't do anything.

Below are the revelevant VF and apex chunks

 

APEX

 

    public void getData()
    {
        //This function is responsible for building a list of respondents based on a master campaign Id

        if(respondents != null)
        {
            respondents.clear();
        }        
        //Find the Id of the selected campaign
        campaignId = Apexpages.currentPage().getParameters().get('campaignId');
        
        //Find if the user has entered any kind of search string
        searchQueryString = Apexpages.currentPage().getParameters().get('respondentSearch');
        if(searchQueryString == null)
        {
            searchQueryString = '%';
        }
        else
        {
            searchQueryString = '%' + searchQueryString + '%';
        }
        
        system.debug('------------------------------------------------------------------------------------------------');
        system.debug('Searching for '+searchQueryString);
        system.debug('------------------------------------------------------------------------------------------------');
        
        //If the campaignId has been populated
        if(campaignId != null)
        {
            system.debug('------------------------------------------------------------------------------------------------');
            system.debug('Searching for respondents in'+campaignId);
            system.debug('------------------------------------------------------------------------------------------------');        
            respondents = [SELECT Id,
                                Respondent_Check_In_Status__c,
                                Respondent__r.FirstName,
                                Respondent__r.Id,
                                Respondent__r.LastName,
                                Respondent__r.PID__c,
                                Respondent__r.Gender__c,
                                Respondent__r.Phone,
                                Respondent__r.Email,
                                Respondent__r.Birthdate,
                                Respondent__r.Organization1__c,
                                Respondent__r.HiddenOrgName__c,
                                Respondent__r.Age__c,
                                Criteria_1_Hidden__c,
                                Criteria_2_Hidden__c,
                                Criteria_3_Hidden__c,
                                Criteria_4_Hidden__c,
                                Criteria_5_Hidden__c,
                                Criteria_6_Hidden__c,
                                Criteria_7_Hidden__c,
                                Criteria_8_Hidden__c,
                                Criteria_9_Hidden__c,
                                Criteria_10_Hidden__c,                              
                                Child_Campaign__r.Incentive_Amount_Paid_for_Study__c,
                                Child_Campaign__r.Time_Slot__c,
                                Master_Campaign__r.Contribution_Amount__c,
                                Master_Campaign__r.Criteria_1__c,
                                Master_Campaign__r.Criteria_2__c,
                                Master_Campaign__r.Criteria_3__c,
                                Master_Campaign__r.Criteria_4__c,
                                Master_Campaign__r.Criteria_5__c,
                                Master_Campaign__r.Criteria_6__c,
                                Master_Campaign__r.Criteria_7__c,
                                Master_Campaign__r.Criteria_8__c,
                                Master_Campaign__r.Criteria_9__c,
                                Master_Campaign__r.Criteria_10__c,
                                Respondent__r.MailingStreet,
                                Respondent__r.MailingPostalCode,
                                Respondent__r.MailingState,
                                Respondent__r.MailingCity,
                                Respondent__r.MobilePhone
                                From Respondent__C where Master_Campaign__c = :campaignId and Respondent__r.firstname like :searchQueryString];       
        }    

    }

 

 

 

VF

 

<apex:actionFunction action="{!getData}" name="filterRespondentList" status="filtering" reRender="cmTable">

        <apex:param name="respondentSearch" value="" />
 </apex:actionFunction>
            
                          
 Find Respondent: <input type="text" id="respondentName" />
<apex:commandButton title="Find" value="Find"  onclick="filterRespondentList(respondentName.value);" />

TehNrdTehNrd

Heck, if your table initially loads with all of the data I'd probably just filter it in the browser with javascript rather than having the server do this. Wouldn't be that hard to iterate over the table rows and and set display:none. Even easier with something like jQuery.

Kenji775Kenji775

 like the jQuery solution, but it's hard to isolate elements in VF because of the crazy ID's it gives everything as far as I know. Any tips for if I approach it this way? I can probably hack something together, but my guess is anything you got is probably a lot more elegant than whatever I'll come up with.

TehNrdTehNrd

This visualforce markup...

 

Filter: <input type="text" id="fieldFilter"/>
<apex:pageBlockTable value="{!fields}" var="f" cellpadding="0" cellspacing="0" width="100%" id="fieldTable">
	<apex:column headerValue="Field Label" value="{!f.fld.Label}" id="fieldLabel"/>
	<apex:column headerValue="Field Name" value="{!f.fld.Name}" id="fieldName"/>
</apex:pageBlockTable>

....will render this HTML....

 

<table width="100%" cellspacing="0" cellpadding="0" border="0" id="page:form:fieldBlock:fieldTable" class="list ">
  <colgroup span="3">
  </colgroup>
  <thead class="rich-table-thead">
    <tr class="headerRow ">
      <th id="page:form:fieldBlock:fieldTable:fieldLabelheader" colspan="1" scope="col" class="headerRow">
        <div id="page:form:fieldBlock:fieldTable:fieldLabelheader:sortDiv">Header1</div>
      </th>
      <th id="page:form:fieldBlock:fieldTable:fieldNameheader" colspan="1" scope="col" class="headerRow">
        <div id="page:form:fieldBlock:fieldTable:fieldNameheader:sortDiv">Header2</div>
      </th>
    </tr>
  </thead>
  <tbody id="page:form:fieldBlock:fieldTable:tb">
    <tr onfocus="if (window.hiOn){hiOn(this);}" onblur="if (window.hiOff){hiOff(this);}" onmouseout="if (window.hiOff){hiOff(this);} " onmouseover="if (window.hiOn){hiOn(this);} " class="dataRow even  first">
      <td colspan="1" id="page:form:fieldBlock:fieldTable:0:fieldLabel" class="dataCell">somevalue1</td>
      <td colspan="1" id="page:form:fieldBlock:fieldTable:0:fieldName" class="dataCell">somevalue2</td>
    </tr>
    <tr onfocus="if (window.hiOn){hiOn(this);}" onblur="if (window.hiOff){hiOff(this);}" onmouseout="if (window.hiOff){hiOff(this);} " onmouseover="if (window.hiOn){hiOn(this);} " class="dataRow odd">
      <td colspan="1" id="page:form:fieldBlock:fieldTable:1:fieldLabel" class="dataCell">somevalue3</td>
      <td colspan="1" id="page:form:fieldBlock:fieldTable:1:fieldName" class="dataCell">somevalue4</td>
    </tr>
    <tr onfocus="if (window.hiOn){hiOn(this);}" onblur="if (window.hiOff){hiOff(this);}" onmouseout="if (window.hiOff){hiOff(this);} " onmouseover="if (window.hiOn){hiOn(this);} " class="dataRow even">
      <td colspan="1" id="page:form:fieldBlock:fieldTable:2:fieldLabel" class="dataCell">somevalue</td>
      <td colspan="1" id="page:form:fieldBlock:fieldTable:2:fieldName" class="dataCell">somevalue</td>
    </tr>
  </tbody>
</table>

 ...and this jQuery function will filter the table....

 

j$("#fieldFilter").live('keyup', function() {
    var filter = j$(this).val().toLowerCase();
    
    //TABLE Logic
    if(filter){
        //Find table body and iterated over each <tr>
        j$("#page\\:form\\:fieldBlock\\:fieldTable\\:tb").children("tr").each(function(e) {
            //Find values in table, we are in loop and can use 'e' variable as index count, use it to build selector and
            //get the correct values from the <tr>
            var label = j$("#page\\:form\\:fieldBlock\\:fieldTable\\:"+e+"\\:fieldLabel").text().toLowerCase();
            var name = j$("#page\\:form\\:fieldBlock\\:fieldTable\\:"+e+"\\:fieldName").text().toLowerCase();

            //If values contains filter text, show it, else hide it    
            if(label.indexOf(filter) >= 0 || name.indexOf(filter) >= 0){
                j$(this).show(); //j$(this) represents the <tr> element we are iterating over
            }else{
                j$(this).hide();
            }
        });
    }else{
       //if filter value is blank show all table rows
       j$("#page\\:form\\:fieldBlock\\:fieldTable\\:tb").children("tr").show();
    }
});

Your IDs will most likely be different and you will need to updated the jQuery but this should get you well on your way.

 

-Jason

 

Kenji775Kenji775

Thank you sir, you are a gentleman and a scholar ;)

I really appreciate it. I'll let ya know if I have questions.

d3developerd3developer

Ken,

 

I highly suggest using the styleClass to give your elements unique ids of your own choosing and then referencing them with ('.myUniqueId').doSomething(); 

Kenji775Kenji775

That's a really good idea. I forgot jQuery can select based on css class, so yeah, just assign my elements some specific class and use that to select them. Good thinking! Thanks!

d3developerd3developer

Notes: (1) only a tiny bit slower than the id. (2) works on virtually all VF elements (except inputFields)

TehNrdTehNrd

Style class is an option but I had this example laying around and it is tuned for performance over developer friendliness. Finding elements by class name can be a lot (i think more than tiny) slower on older browsers.

Kenji775Kenji775

I do remember selecting based on css class to be slower, though this is going to be an internal only application so  I can control what browsers are getting to it. I'll certainly keep it in mind. Either way, it's good to have options, so thanks to you both for your exellent insight.

d3developerd3developer

I had a conversation about this in the jQuery IRC room since I wasn't sure about the speed and had read a little about it elsewhere. The conclusion (including input from jQuery core team members) is that since the css style attribute selection is handled by jQuery it is only marginally slower and will barely affect performance. This is true in my experience although I've not spent a lot of time in older browsers.

 

Thanks Kenji! Good luck!

TehNrdTehNrd

Intresting to hear. From what I understood jQuery uses the native getElementByClassName (very fast) if it is supported by browser (everything but IE) and if not it must traverse the entire DOM. May not be that bad if you are only getting one element but if used in a loop milliseconds can add up fast.

 

EDIT: Confirmed, it does use native getElementsByClassName if supported: http://api.jquery.com/class-selector/

d3developerd3developer

Makes sense. Probably worth doing some diagnostics somehow at some point. Plz post to your blog if you do and I'll do the same.