12 July 2012

CiviCRM profile search block

On a CiviCRM 4 site I help run, there is a profile search form exposed to authorised public users of the Drupal site.  This form provides many search options including the selection of a region of the UK.

My task is to provide a Drupal block on the site home page that shows a map of the UK with each region clickable - when you click, a map of the results is shown, not a text listing of results.

My solution was to build on the existing profile, as it's display templates have been customised in various ways.  To work out what to do, you will have to analyse what is generated by CiviCRM for the actual search form, in particular the (custom) fields that are used.

Basic block structure

My block creates an HTML form that is posts to CiviCRM.  Civi uses session keys to differentiate between each user's search.  A crucial task is therefore to set a valid "qfKey" hidden form parameter.  To get a suitable key, the PHP code creates a suitable form controller and retrieves the key.  The code below shows the PHP and a skeleton of the form itself.

  require_once 'CRM/Core/Controller/Simple.php';

  $formController = new CRM_Core_Controller_Simple( 'CRM_Profile_Form_Search', ts('Search Profile'), CRM_Core_Action::ADD );
  $formController->setEmbedded( true );
  $formController->set( 'gid', 123 );

  $ky = $formController->_key;

  <form  action="/civicrm/profile?gid=123" method="post" name="RegionMap" id="RegionMap">
  <input name="qfKey" type="hidden" value="<?php echo $ky ?>" />
  <input name="_qf_default" type="hidden" value="Search:refresh" />


  <input name="_qf_Search_refresh" value="Search" type="submit"/>

Form contents

To work out what needs to go in the form, you will have to copy what's in the standard search form generated by CiviCRM.  It seems like you only need to include the chunks for the sub-search you wish to do, ie you do not need to have dummy/hidden entries for all fields.

For my region search, the standard form output consists of a hidden INPUT, a checkbox INPUT and an associated LABEL.  You can slim it right down to one hidden INPUT like this:

  <input type="hidden" id="northeast" name="custom_21[1.00]" value="" />

"custom_21" is the Civi custom data field internal name.  The string in square brackets is the field value - in my case, one of the mulitple choice option values.  I have added the "northeast" id to this INPUT.

If you set the value for the above hidden field to "1" and submit the form, you should see the standard text listing of results that match that search.  On that page, there should be a "Map these contacts" link.

Showing the result map straight away

To show a map of results straight away, you'd think you'd simply copy the URL of the map page, and it would work:

<form action="/civicrm/profile/map?map=1&gid=123&reset=1" method="post" name="RegionMap" id="RegionMap" >

You do need to do that change, but by itself it doesn't work.  If you try it, you will find that it always uses the last proper search terms that you used.  Civi is storing the last search parameters in the "profileParams" session variable.  So, our first task is to clear this session variable when our block is initialised, ie in the above code:

  $session = CRM_Core_Session::singleton();

Here's how I set the profileParams session value.  First, I created another hidden field which will be set by JavsScript to contain the value chosen on my block:

<input name="regionCode" type="hidden" value="" />

In the custom CiviCRM code for my site, I amended CRM/Profile/Page/Listings.php in getProfileContact() to rebuild the profileParams session value if my new hidden variable is set:

$regionCode = CRM_Utils_Array::value( 'regionCode', $_REQUEST );
if ( isset($regionCode))
  $params = array();
  $custom_21 = array();
  $custom_21[$regionCode] = 1;
  $params["custom_21"] = $custom_21;

For this to work, the regionCode hidden field needs to be set to a suitable value, such as "1.00" in my case.

And the block image map...

The block uses a standard HTML image map along with JavaScript and jQuery to submit the form when the user clicks on a region.  The map area code looks like the following:

  <area shape="poly" coords="149,123,158,101,205,169,187,166,181,190" href="/" alt="North East" title="North East England" onclick="return ShowRegion('northeast');" />

The following JavaScript is used to respond to map clicks:

<script type="text/javascript">
  var cj = jQuery.noConflict(true);

  function ShowRegion(regionname){
    var regionField = cj('input[id="'+regionname+'"]');

    var regionFieldName = regionField.attr("name");
    var regionCode = null;
    var lbpos = regionFieldName.indexOf("[");
    if( lbpos!==-1){
      var rbpos = regionFieldName.indexOf("]",lbpos);
      if( lbpos!==-1){
        regionCode = regionFieldName.substring(lbpos+1,rbpos);
    if( regionCode==null){
      alert("Sorry, no regionCode found");
      return false;

    var regionSpecialField = cj('input[name="regionCode"]');

    return false;