Extended Autocomplete Helper

When I started with CakePHP, my first project includes also Autocomplete functionality for some fields. Of course I start using default Ajax Helper provided from the library. The main problem was that in most of the cases I needed the ID of the string which I searched. The best example is the Country list which is very easy to be accessed with Autocomplete, but instead to messing up with strings as usual developer I wanted to have the ID of the specified country. I used some kind of hack by adding callback function onComplete, which checks the selection in the Autocomplete field, and by this selection set hidden ID field, but as you can imagine this is not the best choice, first of all because it’s a hack – you rely on a sting which already sound as a hack /imagine if you are searching in a list there are some duplicated entries – examples are too many/ and second if you rely on callback this mean that you need another second or two for the second responce. Anyway, by giving this examples just wanted to convince you once again that Autocomplete helper with Key and Value related are really necessary in the projects /or at least that’s what I am thinking./

Well so far the default Autocomplete helper doesn’t provide such functionality. That’s why I created this helper which solves this problem partly. Why partly? You will see the answer of this in Strict Autocomplete with Scriptaculous (Part II)

Ok, let’s taste the my recipe.

Here is the helper:

/**
* Autocomplete Helper
*
* @author  Nik Chankov
* @website http://nik.chankov.net
* @version 1.0.0
*/


class StrictAutocompleteHelper extends AjaxHelper {
   
    var $helpers =  new Array('Form');
    /**
    * The Main Function for autocomplete
    *
    * @param string $field Name of the database field. Possible usage with Model.
    * @param string $url Url of the Ajax Request. Possible null. Then the Ajax will get the current URL.
    * @param array $options Optional Array. Potions are the same as in the usual text input field.
    * Additional option is to determine autocomplete as ID or not - boolean $options['strict'].
    */

    function autoComplete($field, $url = null, $options = array()){
        $var = '';
        $idField = '';
        $checkField = '';
        $strict = true;
       
        //Get Ajax.Autocomplete in a var /if the variable name is set/
       
        if (isset($options['var'])) {
            $var = 'var ' . $options['var'] . ' = ';
            unset($options['var']);
        }
       
        if (isset($options['strict'])) {
            $strict = $options['strict'];
            unset($options['strict']);
        }
       
        //Equalize the URL if empty to current URL
        if($url === null){
            $url = $this->Html->params['url']['url'];
        }
       
        //Camelize the ID
        if (!isset($options['id'])) {
            $options['id'] = Inflector::camelize(r("/", "_", $field));
        }
       
        //Add id and check fields ids
        $options_id = Inflector::camelize(r("/", "_", $field.'_id'));
        $options_check = Inflector::camelize(r("/", "_", $field.'_check'));
       
        $divOptions = array('id' => $options['id'] . "_autoComplete", 'class' => isset($options['class']) ? $options['class'] : 'auto_complete');
        if (isset($options['div_id'])) {
            $divOptions['id'] = $options['div_id'];
            unset($options['div_id']);
        }

        $htmlOptions = $this->__getHtmlOptions($options);
        $htmlOptions['autocomplete'] = "off";
        if($strict == true){
            $options['afterUpdateElement'] = "function(text, li){\$('".$options_id."').value = li.id;\$('".$options_check."').value = text.value;}";
        }
       
        foreach ($this->autoCompleteOptions as $opt) {
            unset($htmlOptions[$opt]);
        }

        if (isset($options['tokens'])) {
            if (is_array($options['tokens'])) {
                $options['tokens'] = $this->Javascript->object($options['tokens']);
            } else {
                $options['tokens'] = '"' . $options['tokens'] . '"';
            }
        }

        $options = $this->_optionsToString($options, array('paramName', 'indicator'));
        $options = $this->_buildOptions($options, $this->autoCompleteOptions);
       
        //preparing Fields
        if($strict == true){
            $idField    = $this->Form->input($field.'_id', array('label'=>'field id', 'id'=>$options_id, 'type'=>'hidden'));
            $checkField = $this->Form->input($field.'_check', array('label'=>'field check', 'id'=>$options_check, 'type'=>'hidden'));
        }
       
        $oriField   = $this->Form->input($field, $htmlOptions);
       
        //prepare the output
        $return = "";
       
        $return .=
            $oriField.
            $idField.
            $checkField.
            $this->Html->div(null, '', $divOptions) . "\n" .
            $this->Javascript->codeBlock("{$var}new Ajax.Autocompleter('" . $htmlOptions['id']
                . "', '" . $divOptions['id'] . "', '" . $this->Html->url($url) . "', " .
                        $options . ");"). "\n";
           
        if($strict == true){
            $return .= $this->Javascript->codeBlock("
                    Event.observe("
".$htmlOptions['id']."", "blur", function (event){
                            var element = Event.element(event);
                            if($(element.id).value != $('"
.$options_check."').value){
                                $(element.id).value = '';
                                $('"
.$options_id."').value = '';
                                $('"
.$options_check."').value = '';
                            }
                        }
                    );
                    //Clean the ID and Check if there is change in the Autocomplete Field
                    Event.observe("
".$htmlOptions['id']."", "keyup", function (event){
                            var element = Event.element(event);
                            if($(element.id).value != $('"
.$options_check."').value){
                                //$(element.id).value = '';
                                $('"
.$options_id."').value = '';
                                $('"
.$options_check."').value = '';
                            }
                        }
                    );
            "
);
        }
        return $return;
    }
}

Here is how to use it:

In the controller you should add this helper in $helpers class variable:

class TestController extends AppController {
...
var $helpers = array('Html', 'Form', 'Ajax', 'Javascript', 'StrictAutocomplete');
...
}

Then in the View you should add following line:

<?php echo $strictAutocomplete->autoComplete('autocomplete', 'ajax_autocomplete', array('label'=>'Demo Autocomplete Strict', 'strict'=>true));?>

Where:

  • ‘autocomplete’ is name of the field. The format could be also Model/field instead just field.
  • ‘ajax_autocomplete’ is the url of the Ajax Response. In that Example the url is in the same controller. It’s also possible to leave this variable null. Then it will take the current url as Ajax Response url.
  • third parameter is the options tag, which is the same as normal Form->input(field, options) field.

The only difference between them is the extra parameter:

$options['strict'] = true; //boolean

which determine the field as well known behaviour of the Autocomplete helper /false/, or the “Strict” usage when also ID is needed /true/.

So far this is not complete Ajax replica of the Select HTML tag, because there are few know issues. If you want to know more you should take a look on Strict Autocomplete with Scriptaculous (Part II) article in my blog where I explained what are the bugs and the ways to escape them.

You can see that Helper in action, slightly modified so you can see the control fields, while in the real helper they are hidden – here

Hope this helps someobody.

47 thoughts on “Extended Autocomplete Helper

  1. Tim

    Hey Guy,nice little thing…..okay,it looks like its nice,but it actually doesnt work.
    I always get these two errors:
    Notice: Undefined property: StrictAutocompleteHelper::$Form in /Users/timbuchwaldt/Sites/cake/app/views/helpers/strict_autocomplete.php on line 73

    Fatal error: Call to a member function input() on a non-object in /Users/timbuchwaldt/Sites/cake/app/views/helpers/strict_autocomplete.php on line 73

    when i use it. Maybe i did something wrong,so here is what i did:
    added the line with the helpers-array, added this

    autoComplete(‘autocomplete_actor’, ‘Movie/autocomplete’, array(‘label’=>’Demo Autocomplete Strict’, ‘strict’=>true))?>

    to my index view , im having the autocomplete function(i use the one from cakephp.org)

  2. Nik Chankov Post author

    Hi Tim,

    this error could be caused only from one thing: Form helper is not attached in the class. I already modified the helper by adding:

    var $helpers =  new Array('Form');

    which should solve this fatal error.

    Hope you working with CakePHP 1.2, because this helper is written for that version of Cake. ๐Ÿ™‚

  3. huzz

    thanks for sharing…
    however i still confuse where & how to put the ID (keys for displayed autocomplete items)? Am I miss something?

  4. Nik Chankov Post author

    Huzz,

    helper just set the field from the first parameter of the function to be hidden and visible autocomplete field is just for visualization, and should not be used in the processing part in the controller.
    The important part is the second parameter of the function /which is the url which should be called when the user start typing in the field/ and it should point to an action with ajax layout with format like this:

    <ul>
       <li id="1">Value 1</li>
       <li id="2">Value 2</li>
       <li id="3">Value 3</li>
       ...
       <li id="n">Value N</li>
    </ul>

    I leave to your imagination how to create it with controller and view ๐Ÿ™‚
    of course you can see the example:
    http://nik.chankov.net/examples/playground/test/autocomplete

    Bear in mind that this is working with modified version of scriptaculous, so it’s not 100% compliant to the last version of scriptaculous.

    Hope this make more sense.

  5. Nik Chankov Post author

    #Cory – could you tell me where you experiencing problems with this? Bear in mind that Scriptaculous js is a modified version and is not 100% the same as latest version on the Script.aculo.us site and I guess that the problem is this.

    Another hint I give in my previous comment – where I explained what should return the url specified in the second parameter.

  6. helloworld

    newbie here..

    Error:
    Fatal error: Call to a member function div() on a non-object in APP\views\helpers\strict_autocomplete.php on line 95

    i’m lost. what should i do?

    about the second parameter.
    could you please elaborate more on that.
    what files do i have to create, and where should i put them?

    thanks in anticipation

  7. Nik Chankov Post author

    So Cory, your error show me that you working on CakePHP1.1.x.x while this and all articles related to CakePHP in this blog are for CakePHP 1.2
    ๐Ÿ™‚

    About the second parameter:
    if you see this url:
    http://nik.chankov.net/examples/playground/test/ajax_autocomplete
    this is the second parameter’s page.

    The code generated this page is similar to this:

        function ajax_autocomplete(){
            $this->layout = 'ajax';
            $this->cleaner = new Sanitize();
            $this->data = $this->cleaner->clean($this->data);
           
            $countries = $this->Country->findAll(array("name like '".$this->data[0]['autocomplete_auto']."%'"));
            $this->set('countries', $countries);
        }

    and then make one view which will do ul and li tags.

    Hope now is more clear…

  8. helloninio

    thanks for the ajax_autocomplete…

    so i tried adding the code to my TestController and creating one view that contains ul and li tags..

    but, i still have the error:
    Fatal error: Call to a member function div() on a non-object in APP\views\helpers\strict_autocomplete.php on line 95

    what is causing this ?
    what should i do to resolve this problem ?

  9. Nik Chankov Post author

    As I told you – you using CakePHP v 1.1 while I am using CakePHP 1.2.

    The error coming, because in 1.1 there is no function div() in the html helper.

    if you just start using CakePHP I strongly recommend you to switch to 1.2 because it’s more reach and this is the future of the framework. 1.1 just support old apps which already used it.

  10. Pingback: Javi Sanromรกn web log » Componentes y helpers en cakephp

  11. Nils Ellingsen

    Got PHP errors with CakePHP v1.2.0.7125 RC1, changed line 12:

    var $helpers = new Array(‘Form’);
    change to:
    var $helpers = Array(‘Form’,’Html’,’Javascript’);

  12. james

    suppose i have two autocomplete textfields on the same page. the list of the second textfield depends on the selected id of the first textfield.

    it’s like an additional condition will be added to the find() function of the second textfield depending on the selected id of the first textfield.

    question.. how can i dynamically get the id of the first textfield to be passed on to the find() of the second textfield.

    i tried, $this->data[‘Model1’][‘textfield1_id’], but it returns an empty string.

  13. Nik Chankov Post author

    Hi James,

    if you using my approach /where there is a hidden field which handle the IDs/ it will be pretty easy. Just oncomplete /when the ajax request of the first field is complete/ fire the second ajax request with the value of the hidden field of the first autocomplete.

    I don’t know if I was very clear, but if you have more questions fire them up ๐Ÿ™‚

  14. james

    ok. i got your idea.

    but i dont know how to put it into code…
    especially the oncomplete part.
    do i have to modify the scriptaculous codes?
    or just the controller part?

    thanks in advance. ๐Ÿ˜€

  15. Nik Chankov Post author

    James,
    you can attach the Event observer of the first field which will change the url of the second ajax request. let’s say:

    on blur of the field 1 you could have following snippet:

    Event.observe(
      "id_of_the_first_field",
      'blur',
      function(event){
        //Added 1/2 selay to handle data fetching from the server /if you find better way use it/      
        setTimeout( function (){
          ajax_url_second_field += $('id_of_the_first_field_containing_id').value;
        },500)
      }, false
    );

    of course you need to have var ajax_url_countries defined somewhere
    and then get the id from the $this->params in the controller. But I am afraid that on the second field you should use something custom made.

  16. james

    considering your StrictAutocompleteHelper,

    with the variables you used, what refers to the “id_of_the_first_field”? is it the $(element.id).value?

    “id_of_the_first_field_containing_id”?

    does the ajax_url_countries refer to the ajax_autocomplete.ctp, ajax_url_second_field, or ajax_url_first_field?

    how can i integrate it in your StrictAutocompleteHelper? where should i put the new code snippet?

    what about the custom made for the second field?

    oh! sorry about my questions… still learning…

  17. Cam

    Have got it working great, but, could I be missing something, can it be used on an EDIT type page? It clearly works well in a ADD situation.

  18. Cam

    Have solved, be sure to add to the options array of $strictAutocomplete

    “value”=>$this->data[‘Model’][‘field’]

    the edit page will then display the correct data for the field

  19. damian

    hellow nik,
    sorry, iยดm refer if do you have an example of code the view (autocomplete.ctp) thank

  20. Jay

    @Nik Chankov
    I get the samme error.

    I can see in your code:-

    var $helpers = new Array(‘Form’);

    I get the following error at runtime on this line:-

    arse error: parse error in C:\Apache Group\Apache\htdocs\mycake\views\helpers\strict_autocomplete.php on line 12

    I tried with:-

    var $helpers = array(‘Form’);

    instead, but I get the same error:-

    Call to a member function div() on a non-object in [blablabla]

    What do you think?

    Thanks

  21. Jay

    @Jay
    Hum, I added the following to the code:-

    var $helpers = array(‘Form’,’Html’,’Javascript’);

    … Would this be correct?

  22. Nik Chankov Post author

    Yes, in order to use div, which is function of Html helper you have to include Html in this. Although I think that if it’s included in the controller it should be working.

  23. Joey

    Thanks for your work Nik.

    I have 2 questions:-

    ———————————————————-
    A) In the server-side snipet you provide, I can read:-

    $countries = $this->Country->findAll(array(“name like ‘”.$this->data[0][‘autocomplete_auto’].”%'”));

    I don’t understand where ‘autocomplete_auto’ comes from ($this->data[0][‘autocomplete_auto’]).

    I have an issue that the autocomplete does not trigger – if I access the list directly through the action in the browser, which comes to findAll(array(“name like ‘%'”)) basically, I get the list of countries, but when Ajax code accesses it ; which comes to findAll(array(“name like ‘%SOMETHING'”))the list of choices does not appear – I suspect somehow the like clause is broken but I don’t know how to debug this – understanding how what the user entered ends-up in $this->data[0][‘autocomplete_auto’] would be a great help.

    ———————————————————-
    B) When I access a CakePHP action, the returned view always contains the SQL statistics (debug mode 1) – is there a way to remove this from the output, I suspect this might be an extra issue for the autocomplete to work OK.

    Many thanks.

  24. Nik Chankov Post author

    @Joey if you using FireFox you can use Firebug to track what you sending with the Ajax request. With the same tool you can see what you getting from the server as well. Check the console tab of Firebug.

    I can see also that your where condition is wrong instead of

    $this->Model->findAll(array(โ€name like '%SOMETHING'โ€))

    it should be

    $this->Model->findAll(array(โ€name like 'SOMETHING%'โ€))

    It the first case you are searching end of the string, instead of beginning. ๐Ÿ™‚

    About the second question (it may be connected to the first one too) the debug is stopped from /yourapp/app/config/core.php there is a row (the first one) which is: Configure::write(‘debug’, 2); change the value to 1 or 0.

    The reason why I think it’s connected to the first question is because although I am not using prototype in my current projects, I remember that autocomplete expect clear ul li tags, without any other stuff. So it’s possible when you downgrade your debug level this thing could start working :>

  25. Joshua

    Hi Nik, thanks a lot for your efforts.
    Unfortunately, your online demo site seems to have crashed – would it be possible for you to get it back working? I would love to include your helper into my project, but unfortunately I am somewhat stuck right now.
    Thank you
    Joshua

  26. Joshua

    Nik, thank you very much for your immediate response. With your help, I was able to figure out how to use the helper. Anyways, I will have a look at the jQuery-Version as well – although I hesitate to add another JS library to my project…

  27. Joshua

    Nik, sorry to bug you with another question, but maybe you have a bright idea on this one: My autocompleter (your helper displayed above) works perfectly fine in Firefox/Chrome. But IE (6-8) is troubling me: The autocomplete list does not appear immediately after entering text. Instead, it only appears if I enter some text, wait a second and afterwards modify it in any way, e.g. remove a letter.

    Just for demostration purposes:
    – I enter “testquery” (fairly quickly)
    – No reaction
    – I delete the “y”
    – Autocomplete list appears (with the correct entries etc.)

    Do you have any idea what might be causing this strange behavior?

  28. Nik Chankov Post author

    Hi Joshua,

    I’ve tried your suggestion on the example page in my blog, but both on IE6 and 7 there is no such problem. It’s working as expected.

  29. David

    Hi Nik, I would just like to say a big thank you for extending the functionality of the Autocomplete helper with your StrictAutocomplete helper.

  30. Jerome

    … Does not seem to work with labels containing extended characters (ร รฉรจรผ .etc…).
    When I access the controller that generates the list it shows good in the browser, including extended characters, but when this same list displays in the autocompleter, extended characters are replaced by wierd characters … I am stuk for 2 days on this, a little help would be greate ๐Ÿ™‚

  31. Sam

    Hi, I have problems with your helper. The ID doesn’t work… Could you tell me how to write the the function and view of ajax_autocomplete? I think that there is the problem
    ( I’ve already read the part II )
    Thanks

  32. Raajesh

    Hello Nik,
    I have implemented your helper ..but there is no chance for success..Please let me know :
    1. How to write the function for autocomplete in controller..?
    2. How to write view for the same..?

  33. Nik Chankov Post author

    Raajesh, this article is really outdated. Just use jQuery and create a new action in your relevant controller where you can fetch the autocomplete results. ๐Ÿ™‚

  34. Pingback: Componentes y Helpers « Haciendo Tortas

  35. Pingback: Why to Choose CakePHP as Framework | ShapeMyWeb

  36. Pingback: Why to Choose CakePHP as Framework | Symfony GuruSymfony Guru

Leave a Reply

Your email address will not be published. Required fields are marked *