Ideas: NS Web Blog
Valuable tips on maximizing your web presence. Enrich your mind here.

I recently had to modify CiviCRM to support a specific use case for a client. In this project I needed to allow the user to enter anything in the Individual Prefix field, rather than choosing from a list of options. The client in this case was very sensitive about presenting the individual’s honorific precisely as the individual wants, so that “Dr.,” “Rev. Dr.,” and “Most Rt. Rev. Dr.” should all be possible, along with anything else the user might want to enter.
To illustrate the scope of issue: out of the aprox. 20K records in the client’s existing database, there were over 400 distinct values for this field. A select list that long is unwieldy in numerous ways, so we knew we’d have to come up with a solution before importing all those records to CiviCRM.
What I really wanted — and what the client needed — was to present the “Individual Prefix” field as a text input that offers type-ahead auto-complete for a short list of preferred options, but still allows the user to enter whatever they want.
My solution was to implement a custom module which would use two CiviCRM hooks, hook_civicrm_buildForm and hook_civicrm_postProcess, to modify the field display and processing, and add a little custom JavaScript to provide the type-ahead effect.
A quick check on the incredibly helpful, active, and responsive CiviCRM Forums confirmed that my intended solution was probably a good one, so I created the module below. Skip ahead if you just want the code. In the mean time, a little explanation on why it does what it does:
In CiviCRM, the individual prefix is stored as an integer, not as text. This number is value matching the unique ID of an item in CiviCRM’s list of individual prefixes, which are normally configured manually at Administer CiviCRM > Option Lists > Individual Prefixes. Naturally CiviCRM already has a simple way of translating between this stored numeric value and the text that a user sees or enters; in the form for editing an individual contact, that translation happens with an HTML <select> element.
For my modifications to work, the module has to be able to handle that translation itself. To allow the user to enter any arbitrary text, the module must present the user with a text field instead of a <select> element. That text field must be displayed in the right place in the form, and it must display the correct value when the form is first opened if the user is editing an existing contact. When the form is submitted, the module has to accept whatever text the user has entered and match it to one of the items in CiviCRM’s list of individual prefixes — or, if no match exists, it has to add an item to the list and then use that value — and store the correct numeric value as the contact’s prefix.
Finally, at the client’s request, this text field needs to offer some kind of type-ahead auto-complete functionality to steer the user toward a short list of common prefixes. This is accomplished with a simple JavaScript function triggered by the onKeyUp event.
Here, then is the module. I’ve included some lengthy notes in the code comments, explaining some of the unexpected turns I had to take to get the effect I was after. Jump below to see the JavaScript.
<?php /** * Project: TwoMice CiviCRM modifications * File: tmcivicrm.module */ /** * Implementation of hook_civicrm_buildForm() */ function tmcivicrm_civicrm_buildForm($formName, &$form) { if ( $formName == 'CRM_Contact_Form_Contact' && $form->getElementType('prefix_id') == 'select' ) { /* For this form, we'll remove the existing <select> list for the Individual Prefix field and replace it with a text field, allowing the user to enter any value in this field. */ /* Get the text value for the existing prefix, if any */ /* First get the numeric value. getValue() returns an array, or NULL if there is no existing value */ $values = $form->getElement('prefix_id')->getValue(); /* If there is any value for this field (as there will not be when adding a contact) */ /* Get the first element in the array; there should be only one anyway) */ $value = (int)$values[0]; /* Loop through the option lists to find one matching the numeric value, then use the text of that option as the text to display to the user. */ foreach ($form->getElement('prefix_id')->_options as $option) { if($option['attr']['value'] == $value) { $textVal = $option['text']; break; } } } // Remove existing prefix_id field (<select>); we have no need of it. $form->removeElement('prefix_id'); /* Add a couple of new fields for the template. Be sure to assign an * "ID" attribute, so they can be more easily accessed via JQuery in the * template. */ // Add a new prefix_id field, this time a text input. $form->addElement( ); /* Set the value to any existing NUMERIC prefix value. When the form is submitted, CiviCRM will be expecting this value as an integer and will use it to record the Individual Prefix. See tmcivicrm_civicrm_postProcess() for how this works and how it is reconciled with what the user may type in. */ $form->getElement('prefix_id')->setValue($value); /* Add a hidden field, also containing any existing prefix_id text; We will use this in postProcess to check for changed values. */ $form->addElement( 'hidden', 'hidden_prefix_id_text', NULL, ); $form->getElement('hidden_prefix_id_text')->setValue($textVal); /* The ony form element we're NOT adding here is the one the user will * actually see. Instead of adding it here, we use JQuery within the * template to append it in the correct location in the form. This is * the only way we can do it without changing the template itself, since * the template is hard-coded to put in only specific form elements at * specific spots. Any new form elements we add here -- other than * hidden inputs -- will not make it into the template's HTML output. * See the template JavaScript for the code that inserts this element. */ // Insert the JavaScript file that will do the above-described magic. drupal_add_js( drupal_get_path('module', 'tmcivicrm') .'/modules/tmcivicrm/js/Contact_Form_Contact.js' ); } } /** * Implementation of hook_civicrm_postProcess() * * This function performs additional handling of form inputs after CiviCRM has processed the form. */ function tmcivicrm_civicrm_postProcess($formName, &$form) { if ($formName == 'CRM_Contact_Form_Contact') { /* Get the submitted prefix text and any existing prefix text, and compare to see if the prefix has changed. */ $prefix = CRM_Utils_Request::retrieve( 'prefix_id_text', 'String', $form, false, null, 'REQUEST' ); $originalPrefix = CRM_Utils_Request::retrieve( 'hidden_prefix_id_text', 'String', $form, false, null, 'REQUEST' ); /* If the prefix has changed, we'll need to do some work to update it. * If it hasn't changed, it's been saved correctly already, because * the form contained our the numeric value of the existing prefix in * the element prefix_id (added in tmcivicrm_civicrm_buildForm()). */ if ($prefix <> $originalPrefix) { /* Prefix has been changed. Get the correct numeric value for the * new prefix (creating a new Individual Prefix option list item if * necessary), and save the value in the contact record. */ // We'll need this path below for including some files global $civicrm_root; /* Search for the numeric value in case the option list item already exists, and store that as the new prefix numeric value. */ $prefix , CRM_Core_PseudoConstant::individualPrefix() ); /* If no matching option list item was found, we need to add the new * prefix to the Individual Prefix option list. */ if (!$prefixValue) { /* We'll use CiviCRM BAO functions to add the new option. To be sure we're adding to the correct option list (Individual Prefixes and not some other option list), fetch the option group ID from the database. */ $group = new CRM_Core_BAO_OptionGroup; $group->name = 'individual_prefix'; $group->find(true); // Record the correct option group ID $optionGroupId = $group->id; // Prep the correct parameters for the new option list item 'label'=>$prefix, 'name'=>$prefix, 'is_active'=>true ); /* We'll also need these parameters to create the item in the correct option list: */ /* Now use all those parameters and the Option Value functions to add the item to the list. */ $action = CRM_Core_Action::ADD; $option = CRM_Core_OptionValue::addOptionValue( $optionParams, $groupParams, $action, $optionValueID ); /* $option now contains the newly added item, with its numeric value in $option->value. Store that as the correct prefix value. */ $prefixValue = $option->value; /* Now that we've added a new value to the Individual Prefix * option list, we need to refresh CiviCRM's cache of possible * Individual Prefix values. This cache is built once per page, * and it's already been built by the time this hook is fired. * We need to refresh it so that the newly created prefix can be * found and used in the contact's Display Name when we update * the contact record below. */ require_once($civicrm_root . DIR_SEP . 'CRM' . DIR_SEP . 'Core' . DIR_SEP . 'PseudoConstant.php' ); } /* By now, one way or another, we have a numeric value in * $prefixValue. Update the individual record using that value. */ 'contact_id' => $form->_contactId, 'contact_type' => 'Individual', 'prefix_id' => $prefixValue ); require_once( $civicrm_root . DIR_SEP . 'api' . DIR_SEP . 'v2' . DIR_SEP . 'Contact.php' ); $ret = civicrm_contact_add($params); if ($ret['is_error']) { drupal_set_message(t('Could not save changed Individual Prefix. Error: '. $ret['error_message']), 'error'); } } else { /* Prefix is unchanged. Nothing to do, as the value will have been passed in from the form in prefix_id field. */ } } }
The module above makes use of Drupal’s drupal_add_js() function to add the following JavaScript file. Thanks to hershel’s comment for pointing out this method as a way to insert JavaScript code without relying on custom templates techniques. As far as possible I’ve tried to avoid using custom templates in order to preserve forward compatibility for future CiviCRM releases.
This JavaScript code provides both the type-ahead effect this field requires and some small but important data processing when the form is submitted. See the comments for detailed explanation of some points.
<script type="text/javascript"> /* Since we need our custom text element to be in exactly the right * place in the form, namely in the same place as the existing prefix * element, we add it here using JQuery's .append() method, by appending * it to the parent of the prefix element. We give it the same value * as the value of the hidden_prefix_id element (see * tmcivicrm_civicrm_buildForm() in tmcivicrm.module). */ $("#prefix_id").parent().append( '<input type="text" name="prefix_id_text" ' + 'id="prefix_id_text" autocomplete="off" value="' + $('#hidden_prefix_id_text').val() +'">' ); // Add an onKeyUp event handler to the new field. $("#prefix_id_text").keyup(function(event){ prefix_auto_complete(this,event) }); /* Finally, hide (but don't remove) the prefix field, which is actually a text field containing the numeric value of any existing prefix (see tmcivicrm_civicrm_buildForm() in tmcivicrm.module). CiviCRM will process this value when the form is submitted, as explained in tmcivicrm_civicrm_postProcess() in tmcivicrm.module) */ $("#prefix_id").css('display', 'none'); /* I had this function lying around from previous projects. How it works is not especially relevant to this article, but I leave it here for your reference. */ function prefix_auto_complete(element, event) { titles = new Array( "Rev.", "Dr.", "Prof.", "Father", "Mr.", "Mrs.", "Ms.", "Hon.", "Ven.", "H.E.", "Bishop", "Sheikh", "Imam" ); if (element.value == "") return 0; /* if it's a backspace (keycode=8), just return (we do nothing on a backspace)*/ if (event.keyCode == 8 ){ return 0; } /* Find out if element has the function createTextRange (otherwise we should use setSelectionRange()) */ if (element.createTextRange) { useTextRange = true; } else { useTextRange = false; } /* set up some temp variables */ origValue = element.value; // if the box is empty, just return. if (origValue == "")return 0; // cycle through each element of Array titles() until we get a match: for (n=0; n < titles.length; n++){ title = titles[n]; count = 0; for (i = 0; i < origValue.length; i++){ if ( title.toLowerCase().charAt(i) == origValue.toLowerCase().charAt(i) ){ count++ } } /* if count == origValue.length, we know all the characters matched, so we have a match! */ if (count == origValue.length){ /* find out how many characters remain to be filled in by auto-completion */ diff = title.length - origValue.length; if (diff <= 0) break; /* remainingChars is going to be a string of the remaining characters to be filled in */ remainingChars = ""; for (i=0; i < title.length; i++){ if (i >= origValue.length) { remainingChars += title.charAt(i); } } element.backspace = true; if (useTextRange) { // If useTextRange (Internet Explorer, et al) var textRange = element.createTextRange(); /* truncate the matching array item to the length of what was typed */ textRange.text=title.substr(0,textRange.text.length); // then we add remainingChars, select it, and we're done! textRange.text += remainingChars; textRange.findText(remainingChars,diff*-2); textRange.select(); return 0; } else { // (Mozilla, et al) element.value = title.substr(0,origValue.length) + remainingChars element.setSelectionRange( origValue.length, remainingChars.length + origValue.length ); return 0; } } } element.backspace = false; return 0; } </script>
This solution is working well for me now. I’m sure there are ways to improve it. I look forward to any suggestions for improvements in the code above or in the general approach.
Comments (5)
hey, you should repost this
hey,
you should repost this on the civicrm blog if you have time. any ideas for making it easier for you to avoid customising the template in this case?
I’d be happy to
@Michael: I’d be happy to put it on the CiviCRM blog. Any idea how I can do that?
As for avoiding customized templates: if there were a way to inject code into a given template, that’s all that’s needed. A system I’ve used before uses naming conventions to automatically load like-named JS files for a template, so that template1.tpl always looks for and includes template1.js. This doesn’t seem to be the Civi way, though. Better for CiviCRM might be some way to modify template code using hook_civicrm_pageLoad — is that already possible? If we could do that I might never customize a template again.
Allen - great post. I’ve
Allen - great post. I’ve added blogging rights to your account on civicrm.org - so u can create a blog a repost by going to your My Account page.
Thanks
Thanks, David. I’ve added the blog post here: http://civicrm.org/blogs/allenshaw/using-hooks-change-individual-prefix-...
> Thanks to hershel’s comment
> Thanks to hershel’s comment for pointing out this method as a way to insert JavaScript code without relying on custom templates techniques.
You’re welcome. :)
Add a comment