Home > Ajax, CakePHP, Development, Frameworks, General, PHP > Extended Autocomplete Helper

Extended Autocomplete Helper

September 3rd, 2007

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.

Ajax, CakePHP, Development, Frameworks, General, PHP , , , , , , ,

  1. February 15th, 2008 at 20:59 | #1

    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. February 15th, 2008 at 21:43 | #2

    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
    February 18th, 2008 at 22:23 | #3

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

  4. February 18th, 2008 at 23:27 | #4

    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. Cory
    March 5th, 2008 at 22:18 | #5

    I am a noob to Cake and I have gotten this to work. But is there a way to post exact code used to generate your:
    http://nik.chankov.net/examples/playground/test/autocomplete

    page? I would like to duplicate this page exactly and I am having some problems

  6. March 6th, 2008 at 11:16 | #6

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

  7. helloworld
    March 6th, 2008 at 12:14 | #7

    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

  8. March 6th, 2008 at 13:10 | #8

    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…

  9. helloninio
    March 7th, 2008 at 08:21 | #9

    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 ?

  10. March 10th, 2008 at 11:34 | #10

    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.

  11. June 30th, 2008 at 14:59 | #11

    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. June 30th, 2008 at 16:44 | #12

    I don’t have such error. But I checked my code and look like I’ve added these helpers in my controller class ;)

  13. July 21st, 2008 at 17:29 | #13

    works great

  14. james
    August 14th, 2008 at 10:57 | #14

    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.

  15. August 14th, 2008 at 11:11 | #15

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

  16. james
    August 14th, 2008 at 11:33 | #16

    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. :D

  17. August 14th, 2008 at 12:01 | #17

    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.

  18. james
    August 19th, 2008 at 08:36 | #18

    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…

  19. Cam
    November 24th, 2008 at 14:27 | #19

    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.

  20. November 24th, 2008 at 16:10 | #20

    @Cam, it definitely can be used for Edit action. It will get the value from the $this->data array without any problem.

  21. Cam
    November 29th, 2008 at 08:35 | #21

    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

  22. December 9th, 2008 at 19:19 | #22

    Hello Nik,

    Thanks for the great work! My Autocompleter works brilliantly, thanks to you!

    Max.

  23. damian
    January 6th, 2009 at 22:06 | #23

    hello Nik, do you have an example of view for ajax_autocomplete
    Thank you

  24. January 6th, 2009 at 23:02 | #24

    Sure there is. http://nik.chankov.net/examples/playground/test/autocomplete – the link was at the end of the article ;)

  25. damian
    January 7th, 2009 at 01:03 | #25

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

  26. January 12th, 2009 at 23:24 | #26

    Here it is, although it’s not verry different from what I explained in the post: http://nik.chankov.net/wp-content/uploads/2009/01/autocomplete.ctp ;)

  27. Jay
    February 17th, 2009 at 01:55 | #27

    @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

  28. Jay
    February 17th, 2009 at 02:03 | #28

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

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

    … Would this be correct?

  29. February 17th, 2009 at 10:29 | #29

    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.

  30. Joey
    February 18th, 2009 at 17:53 | #30

    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.

  31. February 18th, 2009 at 22:06 | #31

    @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 :>

  32. Joey
    February 18th, 2009 at 23:30 | #32

    Thanks for the tips, it works like a charm now!

  33. Joshua
    April 6th, 2009 at 18:15 | #33

    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

  34. April 6th, 2009 at 19:46 | #34

    Joshua, I’ve restored the demo, it was because of hosting change and the db config wasn’t changed. Now it’s ok, but I would recommend to start using jQuery instead my helper.

    Probably in the future I will create a demo how to use jQuery, but it’s still in “my head” stage. :) I would recommend this jQuery plugin http://www.pengoworks.com/workshop/jquery/autocomplete.htm

  35. Joshua
    April 6th, 2009 at 21:20 | #35

    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…

  36. Joshua
    April 9th, 2009 at 14:26 | #36

    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?

  37. April 9th, 2009 at 16:10 | #37

    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.

  1. October 18th, 2007 at 02:57 | #1
  2. October 21st, 2007 at 10:00 | #2
  3. May 28th, 2008 at 22:00 | #3

*
To prove that you're not a bot, enter this code
Anti-Spam Image