ramblings on PHP, SQL, the web, politics, ultimate frisbee and what else is on in my life
back

Auto complete the world

The current data on search.UN-informed.org is all inserted via an excel sheet importer. In the future we want to skip excel however and do the data entry right in the online database. For this I am working on the admintool. Right now all the tools are generated via the admin generator. However it seems the default dropdown's aren't really made to handle larger data sets. So in the filters and edit forms we run into issues since we have quite a number of tags, documents and clauses already. The numbers will only grow and its simply not feasible to load a list of 3000 options into a drop down. So I started to make use of the sfFormExtraPlugin, which provides a widget for autocomplete. However I had to add some features to really do what we needed.

First up I wanted to add a quick search field into the filters for all admin tools. The idea being that I could simple use the autosuggest to find and load items I want to edit. Unfortunately the default autosuggest widget does not provide any option to pass in custom javascript logic to be executed after the selection so I did a but of search/replace on the standard javascript in a new widget that extends the old one.


<?php
class sfWidgetFormDoctrineJQueryQuickSearchAutocompleter extends sfWidgetFormDoctrineJQueryAutocompleter
{
    public function render($name, $value = null, $attributes = array(), $errors = array())
    {
      $url = url_for('@default_edit?module='.strtolower($this->getOption('model')).'&action=edit&id=XXX');
      $widget = parent::render($name, '', $attributes, $errors);
      $widget = str_replace('.val(data[1]);', ".val(data[1]); if (data[1].replace(/ /, '') !== '') { url = '$url'; location.href = url.replace(/XXX/, data[1]); }", $widget);
      return $widget;
    }
}
?>

So far so good. But we also have plenty of admintools where we need to handle mapping multiple FK's which again would require a drop down with 3000 options. So I created yet another custom widget, this time combining the standard drop down with the autosuggest widget. I customized the getChoices() method in the standard doctrine drop down widget to only fetch the options which are currently selected. In practice I only expect 10-20 options to actually be selected. Then I placed the autosuggest widget above, selecting an item there just adds this to the drop down. Et viola! I can add new mappings without having to load all the possible options at once. It even works together with the double list renderer!


<?php
      $this->widgetSchema['documents_list'] = new sfWidgetFormDoctrineJQueryChoiceAutocompleter(
        array(
          'renderer_class' => 'sfWidgetFormSelectDoubleList',
          'model' => 'Document',
          'multiple' => true,
          'url'   => sfContext::getInstance()->getController()->genUrl('@default?module=document&action=autocomplete'),
        )
      );
?>

Here is the key code I had to implement to get it to work. Its a bit hacky having to search/replace magic once again. Furthermore getChoices() does not know about the selected choices by default nor can it filter on them. So I had to copy that entire method and set a "$this->value" property in the render() method so that I have access to the selected values in getChoices().


<?php
  public function render($name, $value = null, $attributes = array(), $errors = array())
  {
    $this->value = $value;

    $html = '';
    if ($this->getOption('multiple'))
    {
      $attributes['multiple'] = 'multiple';

      if ('[]' != substr($name, -2))
      {
        $name .= '[]';
      }
      $html = '<br />'.parent::render($name, $value, $attributes, $errors);
      $value = null;
    }

    $autocompleter = new sfWidgetFormDoctrineJQueryAutocompleter(
        array(
          'model' => $this->getOption('model'),
          'url'   => $this->getOption('url'),
        )
      );
    $autocompleter = $autocompleter->render($name, $value);

    if ($this->getOption('multiple'))
    {
        // fix trailing _ due to value being an empty string out of toString()
        $autocompleter = str_replace(
            'autocomplete_'.$this->generateId($name).'_',
            'autocomplete_'.$this->generateId($name),
            $autocompleter
        );

        // remove hidden
        $autocompleter = preg_replace(
            '/^[^>]+>(.*)/',
            '$1',
            $autocompleter
        );

        // adjust action on result selection
        $autocompleter = str_replace(
            'jQuery("#'.$this->generateId($name).'").val(data[1]);',
            'if (data[1] > 0) { var dest = document.getElementById("'.$this->generateId($name).'"); dest.options[dest.length] = new Option(data[0], data[1]); }',
            $autocompleter
        );
    }

    return '<strong>Search</strong>: '.$autocompleter.$html;
  }
?>

Here is a short video illustrating how it all works together. The observant viewer will see that in one of the autocomplete boxes I am actually doing a two step process. First the auto completion will allow me to select a document and in the second step I will select a clause inside the document, so the autocompletion code is a bit tricker than in the other modules. This is why I have the "if (data[1] > 0)" in the above javascript code. Given the fact that know next to little about jQuery and all the current javascript frameworks out there, I think its pretty awesome what is possible by tweaking existing symfony widgets!

The next thing I need to do is add a way to load forms for related models via ajax, so that we can speed up the data entry process. For example adding votes for documents currently consists of opening the form for a new vote, selecting a document, selecting a country etc. However I want this to be part of the document edit, especially since the document is linked to an organization, which defines which countries are allowed to vote on the document. If anyone has a suggested on a plugin or code snippet to make this possible let me know. Oh and if you got what it takes to do this, why not just join the effort and submit a patch?