{ Hi! I'm Mike }
I'm a core developer with The Horde Project and a founding parter of Horde LLC - the company behind the world's most flexible groupware platform. This is my personal blog full of random thoughts about development and life in general.
June 16, 2011

The Horde PrettyAutocompleter - Part One

Kronolith 3 includes new tagging features, including an autocomplete feature for adding new tags to events and calendars. Horde has had autocompletion code for ages - autocompletion of email addresses in IMP, for example. In Kronolith, we wanted a more dynamic and fresh interface for tags to go along with the brand new dynamic interface. The result was the PrettyAutocompleter widget. In this entry, I'll explain how it's implemented in Kronolith and how you can adapt it for use in other applications.

 

The PrettyAutocompleter is a stand alone javascript widget that is not limited to tags, in fact, it's also used for attendees in Kronolith's meeting scheduling interface. It's part of the Horde_Core package and lives in prettyautocomplete.js. It extends the Autocompleter object defined in autocomplete.js. You don't have to worry about including any of these files on your own though, the Imple object (see below) takes care of all of this for you.

There are two main components of the autocompleter: The UI and the supporting backend code. First, let's look at the HTML required for the autocompleter. The following is the minimum required to setup an autocompleter.

<input id="kronolithEventTags" name=tags" />
<span id="kronolithEventTags_loading_img" style="display:none;"><img src="spinner.gif" /></span>

Now that we have the HTML setup, let's hook it up to both the javascript that transforms it to the prettyautocompleter and to the backend so it can retrieve the autocomplete choices. For this we use a Horde_Core_Ajax_Imple object. These objects connect UI elements to Ajax actions. Each Horde application can define it's own Imples by adding the classes to application/lib/Ajax/Imple. In the case of Kronolith, we connect it with code similar to this:

$injector->getInstance('Horde_Core_Factory_Imple')->create(
    array('kronolith', 'TagAutoCompleter'),
    array(
        // The name to give the (auto-generated) element that acts as the
        // pseudo textarea.
        'box' => 'kronolithEventACBox',

        // Make it spiffy
        'pretty' => true,

        // The dom id of the existing element to turn into a tag autocompleter
        'triggerId' => 'kronolithEventTags',

        // A variable to assign the autocompleter object to
        'var' => 'eventTagAc'
    )
);

This code transforms the kronolithEventTags element in the above HTML into a PrettyAutocompleter. It also attaches the autocompleter to an Ajax action for retrieving the autocomplete choices. The Imple object that does this is kronolith/lib/Ajax/Imple/TagAutoCompleter.php and it looks like this:

class Kronolith_Ajax_Imple_TagAutoCompleter extends Horde_Core_Ajax_Imple_AutoCompleter
{
    /**
     * Attach the Imple object to a javascript event.
     * If the 'pretty' parameter is empty then we want a
     * traditional autocompleter, otherwise we get a spiffy pretty one.
     *
     * @param array $js_params  See
     *                          Horde_Core_Ajax_Imple_AutoCompleter::_attach().
     *
     * @return array  See Horde_Core_Ajax_Imple_AutoCompleter::_attach().
     */
    protected function _attach($js_params)
    {
        $js_params['indicator'] = $this->_params['triggerId'] . '_loading_img';

        $ret = array(
            'params' => $js_params
        );

        if (empty($this->_params['pretty'])) {
            $ret['ajax'] = 'TagAutoCompleter';
        } else {
            $ret['pretty'] = 'TagAutoCompleter';
        }

        if (!empty($this->_params['var'])) {
            $ret['var'] = $this->_params['var'];
        }

        return $ret;
    }

    /**
     * Method to obtain autocomplete choices. 
     *
     * @param array $args  Arguments passed from the Ajax action. The 'input'
     *                                    parameter contains the text fragment to autocomplete on.
     *
     * @return array  Returns an array of possible choices.
     */
    public function handle($args, $post)
    {
        // Avoid errors if 'input' isn't set and short-circuit empty searches.
        if (empty($args['input']) ||
            !($input = Horde_Util::getFormData($args['input']))) {
            return array();
        }

        $tagger = Kronolith::getTagger();
        return array_values($tagger->listTags($input));
    }

}

Note that there is very little functionality in this class. The bulk of the work is handled by the class it extends - The more general Horde_Core_Ajax_Imple_Autocompleter. The Kronolith_Ajax_Imple_Autocompleter#handle method is automatically called via Ajax by the autocompleter to fetch the choices and then display them in the UI. The $tagger variable is a Kronolith_Tagger object and contains Kronolith specific code for interacting with Horde's Content system. All you need to know right now is that the $tagger->listTags() call provides an array of tag names that start with the text in $input.

The only thing left to do is to initialize the autocompleter. This builds the DOM structure for all the required elements and applies all the styling needed. In Kronolith, this is done when the dynamic interface is loaded (the element remains hidden in Kronolith, however, until the event detail form is displayed).

eventTagAc.init();

The reset() method is used to clear the values from the autocompleter and optionally assign new values. For example, in Kronolith when an existing event is loaded for editing, we prepopulate the autocompleter with that event's existing tags:

// Reset the autocompleter and clear all tags
eventTagAc.reset();

// Reset autocompleter, and prepopulate with two tags.
eventTagAc.reset(['tagOne', 'tagTwo']);

Getting the tags out of the autocompleter to save them is also very easy. However, before actually getting the value out of the autocompleter, we need to make sure that all of it's input is processed. Since the autocompleter uses a comma to trigger adding a new tag to the list of displayed tags, it's possible for the user to type a new tag in the input area, then press save before a adding a comma. We must make sure this last tag is added. This is done by the shutdown() method. Once we are ready to get the value, we can simply access it via the id or name we gave to the initial input element. For example, we gave the input element a dom id of kronolithEventTags and a name of tags. So, to get the current tag value, we can just:

eventTagAc.shutdown();
var tags = $F('kronolithEventTags');

This will be the comma delimited list of tag names.

The following is a bare-bones example script pulling together everything.  Obviously the onload and onclick handlers would normally be written in a less obtrusive way, but for a quick and dirtly example, this is fine. I've written it as if it were part of the Kronolith application. So, if you want to actually test it out and play with the code, just drop it into the root Kronolith directory with a name like example.php. It will pull existing kronolith tag data for the autocompletion, but will obviously not write back out any data.

<?php
/**
 * Autocomplete example
 */

// Setup the application
require_once dirname(__FILE__) . '/lib/Application.php';
Horde_Registry::appInit('kronolith');

// Attach the autocompleter to the ajax action.
// @see Kronolith_Ajax_Imple_TagAutoCompleter
$injector->getInstance('Horde_Core_Factory_Imple')->create(
    array('kronolith', 'TagAutoCompleter'),
    array(
        // The name to give the (auto-generated) element that acts as the
        // pseudo textarea.
        'box' => 'kronolithEventACBox',
        // Make it spiffy
        'pretty' => true,
        // The dom id of the existing element to turn into a tag autocompleter
        'triggerId' => 'kronolithEventTags',

        // A variable to assign the autocompleter object to
        'var' => 'eventTagAc'
    )
);
?>
<head>
 <title>Autocomplete Example</title>
<?php
Horde::includeStylesheetFiles();
Horde::includeScriptFiles();
Horde::outputInlineScript();
?>
<body onload="eventTagAc.init()">
  <div class="kronolithDialogInfo"><?php echo _("To make it easier to find, you can enter comma separated tags related to the event subject.") ?></div>
  <input id="kronolithEventTags" name="tags" />
  <span id="kronolithEventTags_loading_img" style="display:none;"><?php echo Horde::img('loading.gif', _("Loading...")) ?></span>
  <br />
  <a href="#" onclick="eventTagAc.shutdown();alert($F('kronolithEventTags'));">Show me the tags</a>.<br />
  <a href="#" onclick="eventTagAc.reset();">Reset the autocompleter</a><br />
  <a href="#" onclick="eventTagAc.reset(['Personal', 'Fun']);">Reset, with prepopulated tags</a>.
</body>


For the next installment, we'll look at the Previously Used Tags functionality in Kronolith's interface. This is not part of the general PrettyAutocompleter code, but easy to implement.