Skip to Navigation

Drupal Tutorial: Pushing Drupal 6 Core Auto-Complete

Recently I had the task of creating a form that utilized an auto-complete text field that filtered on a specific field of a table. This seemed like a great candidate to use the core auto-complete and after a sprint of information gathering I had what I thought would be enough. After delivery I learned that there was one surprise feature. The auto-complete text field needs to be able to have filters applied to it via-form filter dropdown.

At this point so many things were pointing at going full custom with developing a solution utilizing non-core jQuery solutions. But I had come too far and wanted to keep my core implementation in place. I've shared my findings in this post with hopes that it helps someone else who has this same requirement.

I first created a custom module for this. It's just the simplest most direct way. Don't let that scare you. It's as simple as:

  •  Create a new folder under /sites/all/modules/custom/myModuleName
  • Create a file "myModuleName.info" in that folder (Fill in all the standard information. You can reference a contrib module file)
  • Create a file "myModuleName.module" in that folder (For the actual code.)

Now let's get into implementing this. First I will refer you to the Drupal API Forms.

Example: drupal.org/node/262422

These tutorials by example (Examples 1-10) are awesome. Look at these as I will ignore explaining the basics covered in these. Pay special attention to Example 10 "Multistep" as we will use this to do some special things.

We will build a form that auto-suggests fruits from a database that has an optional filter by fruit color. Once the form is built lets assume we have a text field in our custom form to suggest fruits. We will define this last in our example.

Let's add what we will use for our fruit color filter dropdown:

            // Define fruit color select
            $form['fruit_color'] = array(
              '#type' => 'select',
              '#title' => t('Filter Fruit by Color'),
              '#options' => array(
                '_' => t('Please Select'),
                'red' => t('Red'),
                'yellow' => t('Yellow'),
                'green' => t('Green'),
              ),
            );

 

And of course the apply filter button to use as our action trigger:

            // Define apply_color_filter button
            $form['apply_color_filter'] = array(
              '#name' => 'apply_color_filter',
              '#type' => 'submit',
              '#value' => t('Apply Fruit Color Filter'),
            );

 

Now we will define the auto-complete path this should be done using hook_menu:

            function myModuleName_menu() {
                        // Define Fruit Auto-complete menu item Path
                $items['myModuleName/autocomplete/fruit/%'] = array(
                  'title' => '',
                  'page callback' => '_fruit_autocomplete_names',
                  'page arguments' => array(3),
                  'access arguments' => array('access content'),
                  'type' => MENU_CALLBACK,
                );
                // return menu items
                return $items;
            }
 

This function defines the path to be used by the auto-complete form item as /myModuleName/autocomplete/fruit/% where '%' will represent an argument (the color) passed to the auto-complete function we will define next. It states that the 3rd element "array(3)" in the path will be where the argument is located (0 index based) and will be looking for a callback function named '_fruit_autocomplete_names' to do the auto-complete lookup. Now let's define that function.

            /*
             * Fruit auto-complete search handler for auto-complete path
             */
            function _fruit_autocomplete_names($arg1, $string = '') {
              $matches = array();
              if ($string) {
                // if color filter argument is present in auto-complete path
                if ($arg1 && ("_" != $arg1)) {
                  // Find results where search string is like "fruit_name" and equal to 'fruit_color'
                  // This assumes a table exists in the database called fruit_table that has two fields fruit_name and fruit_color.
                  // This statement will return results where the string from what was currently types in the auto-complete field
                  // is contained in the fruit_name string of a row AND that row has a fruit_color that is equal to our fruit
                  //  color argument passed in. The result will be returned as a sting and there will be a max of 5 results at a
                  //  time. Refer to db_query_range function API for more information.
                  $result = db_query_range("SELECT fruit_name, fruit_color FROM {fruit_table} WHERE LOWER(fruit_name) LIKE LOWER('%%%s%%') AND LOWER(fruit_color) = '" . strtolower($arg1) . "'", $string, 0, 5);
                }
                else {
                  // Find results where search string is like "fruit_name"
                  // This statement functions the same as the above except it skips the 'fruit_color' additional lookup
                  $result = db_query_range("SELECT fruit_name, fruit_color FROM {fruit_table} WHERE LOWER(fruit_name) LIKE LOWER('%%%s%%')", $string, 0, 5);
                }
                // While result rows are returned from the above search
                while ($data = db_fetch_object($result)) {
                  // Add fruit_name to the array of auto-complete suggestions
                  $this_match = check_plain($data->fruit_name);
                  $matches[$this_match] = $this_match;
                }
              }
              // Print results as json array for auto-complete JS to consume and display to the user.
              print drupal_json($matches);
              // Exit before a drupal page render occurs.
              exit;
            }

I documented this function in a few places above to understand what it is doing. Feel free to modify this function as needed to look up your fields in whatever table/field requirements you have. Also please note that it's a little tricky to get a good return value from this. In the example above:

 $matches[$this_match] = $this_match;

describes the structure of the json array. The left side is what the final value will be if a user selects an item from the dropdown list and the right side is what the user sees as dropdown options. These can be the same or different but the tricky part is the user will see both of them at some point (one before selection and the other after selection) so it's a needs to be something that makes sense to the user but can be used to extract the value you need in your submit code. One way I did is adding "[my_id_number]" to the left side var (what they see when the select something) and extract this in my validate function to make sure they didn't change it. I'm open to other suggestions, of course, but this was the only way I could think to do this utilizing the core auto-complete structure still.

Now that we have our auto-complete handler in place we should be able to put the validate path in our URL and see results in json array form that result. Let's say for example we have "Apple","Red" in our fruit_table. We can now put the following on the browser (/myModuleName/autocomplete/fruit/_/Ap) and it could locate that row and add it to the array. Note we use '_' to temporarily hold the argument since I got problems when skipping the argument all together. We could also try to use our color argument and try (/myModuleName/autocomplete/fruit/Red/Ap) which will find the same one. (/myModuleName/autocomplete/fruit/Green/Ap) will not find it because the color argument does not match apple.

Now the last important step in linking this all together is we need to let the text field in our form know to use this same auto-complete path. This is actually pretty simple. When the form first renders, we need to define the text field with the auto-complete path as follows:

            // Define Text field
            $form['fruit_textfield'] = array(
              '#type' => 'textfield',
              '#title' => t('Fruit Name'),
              '#autocomplete_path' => 'myModuleName/autocomplete/fruit/_',
              '#size' => 70,
              '#description' => t('Fruit name description here.')
            );

Note this will be the default auto-complete path for when no color filter has been applied. If you save the form (along with your other form hook definitions from the form API) you can now test typing fruit names in the form and it should complete the fruit names for you. Now we need to create the color filter option.

To do this, refer to the multi-state documentation of the form API examples. What we will do is set a submit variable that say if filter_applied is false, display the text fields defined above. BUT, if filter_applied is true display the text field as follows:

            // Define Text field
            $form['fruit_textfield'] = array(
              '#type' => 'textfield',
              '#title' => t('Fruit Name'),
              '#autocomplete_path' => 'myModuleName/autocomplete/fruit/' . $form_state['values']['fruit_color'],
              '#size' => 70,
              '#description' => t('Fruit name description here.')
            );

This basically says once the form renders and we know the filter_applied flag has been set in our multi-state arguments (use a validation function on the apply_color_filter button we defined above to set this flag to true) then we display this text field instead with an auto-complete path that pulls in the fruit_color value from the color dropdown fields. This all assumes that the proper validation is in place to make sure the apply_color_filter button was checked and the dropdown color itself had a color that was selected by the user. Be careful not to make the color dropdown required because you want to give them the option to not use your helpful color filter if they don't want to.

There are so many possibilities with this method, so I'm not going to go any deeper, but I hope it will save someone a lot of time and searching if you find yourself having to create something like this.

As I said a few times before, I'm sure this can be enhanced and I am open to way to make it more efficient. The string search currently searches any occurence in the fruit_name string. You could change this to a different match pattern to search only the start or end of a string also. You could also increase or decrease the 5 results at a time variable to play with how helpful it is but mind your database load when doing this.

References