CiviCRM: “Individual Prefix” field as auto-complete text input

Note: CiviCRM is a fantastic open-source software package for managing an organization’s contacts, targeted specifically at non-profit organizations. You can learn more about it at CiviCRM.org.

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.

The module

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
<?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 (isset($values)) {
        /* 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(
            'text', 'prefix_id', 'Title', array('id'=>'prefix_id')
        );
        /* 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,
            array('id'=>'hidden_prefix_id_text')
        );
        $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. */
            $prefixValue = array_search(
                $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
                $optionParams = array(
                    'label'=>$prefix, 'name'=>$prefix, 'is_active'=>true
                );
                /* We'll also need these parameters to create the item in the
                correct option list: */
                $groupParams  =array('id'=>$optionGroupId);
 
                /* 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'
                );
                CRM_Core_PseudoConstant::flush('individualPrefix');
            }
 
            /* By now, one way or another, we have a numeric value in
             * $prefixValue. Update the individual record using that value.
             */
            $params = array(
                '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 JavaScript

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
<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>

Conclusion

This solution is working well for me now. I’m sure there are ways to improve it. Since I’m very new to CiviCRM, I look forward to any suggestions for improvements in the code above or in the general approach.

Filed under: CiviCRM — Allen Shaw @ 8:45 pm on

5 Comments »

  1. 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?

    Comment by Michael McAndrew — April 20, 2010 @ 12:10 am

  2. @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.

    Comment by Allen Shaw — April 20, 2010 @ 9:12 am

  3. 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.

    Comment by David Greenberg — April 20, 2010 @ 11:56 am

  4. Thanks, David. I’ve added the blog post here: http://civicrm.org/blogs/allenshaw/using-hooks-change-individual-prefix-auto-complete-text-input

    Comment by Allen Shaw — April 20, 2010 @ 2:04 pm

  5. > 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. :)

    Comment by Hershel Robinson — September 14, 2010 @ 2:38 pm

RSS feed for comments on this post. TrackBack URL

Leave a comment

Powered by WordPress