LogoPhly, boy, phly
the weblog and site of Matthew Weier O'Phinney

Friday, December 12. 2008

Autocompletion with Zend Framework and Dojo

I've fielded several questions about setting up an autocompleter with Zend Framework and Dojo, and decided it was time to create a HOWTO on the subject, particularly as there are some nuances you need to pay attention to.

Which dijits perform autocompletion?

Your first task is selecting an appropriate form element capable of autocompletion. Dijit provides two, ComboBox and FilteringSelect. However, they have different capabilities:

  • ComboBox allows you to enter arbitrary text; if it doesn't match the associated list, it is still considered valid. The text entered is submitted -- not the option value. (This differs from normal dropdown selects.)
  • FilteringSelect also allows you to enter arbitrary text, but it will only be considered valid if it matches an option provided to it. The option value is submitted, just like a normal dropdown select.

Once you've chose the appropriate form element type, you then need to specify a dojo.data store. dojo.data provides a consistent API for data structures consumed by dijits and other dojo components. At it's heart, it's simply an array of arbitrary JSON structures that each contain a common identifier field containing a unique value per item. Internally, both ComboBox and FilteringSelect can utilize dojo.data stores to populate their options and/or provide matches. Dojo provides a variety of dojo.data stores for such purposes.

Defining the form element

Defining the form element is very straightforward. From your Zend_Dojo_Form instance (or your form extending that class), simply call addElement() as usual. Later in this tutorial, depending on the approach you use, you may need to add some information to the element definition, but for now, all that's needed is the most basic of element definitions:


$form->addElement('ComboBox', 'myAutoCompleteField', array(
    'label'     => 'My autocomplete field:',
));
 

Providing data to a dojo.data store

We're going to work backwards now, as providing data to the data store is relatively trivial when using Zend_Dojo_Data.

First, we'll create an action in our controller, and assign the model and the query parameter to the view. We'll be setting up our dojo.data store to send the query string via the GET parameter "q", so that's what we'll assign to the view.


    public function autocompleteAction()
    {
        // First, get the model somehow
        $this->view->model = $this->getModel();

        // Then get the query, defaulting to an empty string
        $this->view->query = $this->_getParam('q', '');
    }
 

Now, let's create the view script. First, we'll disable layouts; second, we'll pass our query to the model; third, we'll instantiate our Zend_Dojo_Data object with the results of our query; and finally, we'll echo the Zend_Dojo_Data instance.


<?php
// Disable layouts
$this->layout()->disableLayout();

// Fetch results from the model; again, merely illustrative
$results = $this->model->query($this->params);

// Now, create a Zend_Dojo_Data object.
// The first parameter is the name of the field that has a
// unique identifier. The second is the dataset. The third
// should be specified for autocompletion, and should be the
// name of the field representing the data to display in the
// dropdown. Note how it corresponds to "name" in the
// AutocompleteReadStore.
$data = new Zend_Dojo_Data('id', $results, 'name');

// Send our output
echo $data;
 

That's really all there is to it. You can actually automate some of this using the AjaxContext action helper, making it even simpler.

Using dojox.data.QueryReadStore

We now have an endpoint for our dojo.data data store, so now we need to determine which store type to use.

dojox.data.QueryReadStore is a fantastic dojo.data store allowing you to create arbitrary queries on data. It creates the query as a JSON object:


{
    query: { name: "A*" },
    queryOptions: { ignoreCase: true },
    sort: [{ attribute: "name", descending: false }],
    start: 0,
    count: 10
}
 

This is problematic in two ways. First, if you were to use it directly, you'd be limited to POST requests, submitting it as a raw post. Second, and related, this means that requests could not be cached client-side.

Fortunately, there's an easy way to correct the situation: extend dojox.data.QueryReadStore and override the fetch method to rewrite the query as a simple GET query with a single parameter.


dojo.provide("custom.AutocompleteReadStore");

dojo.declare(
    "custom.AutocompleteReadStore", // our class name
    dojox.data.QueryReadStore,      // what we're extending
    {
        fetch: function(request) {  // the fetch method
            // set the serverQuery, which sets query string parameters
            request.serverQuery = {q: request.query.name};

            // and then operate as normal:
            return this.inherited("fetch", arguments);
        }
    }
);
 

The question now is, where to create this definition?

You have two options: you can inline the custom definition (less intuitive) and connect the data store manually to the form element, or you can create an actual javascript class file (slightly more work) and have your form element setup the data store for you.

Inlining a custom QueryReadStore class extension

Inlining is a bit tricky to accomplish, as you need to declare things in the appropriate order. When using this technique, you need to do the following:

  1. require the dojox.data.QueryReadStore class
  2. define a global JS variable that will be used to identify your store
  3. use dojo.provide and dojo.declare to create your custom data store extension
  4. define an onLoad event that instantiates the data store and attaches it to the form element

We can do all the above within the same view script in which we spit out our form:


<?php
$this->dojo()->requireModule("dojox.data.QueryReadStore");

// Define a new data store class, and
// setup our autocompleter data store
$this->dojo()->javascriptCaptureStart() ?>
dojo.provide("custom.AutocompleteReadStore");
dojo.declare(
    "custom.AutocompleteReadStore",
    dojox.data.QueryReadStore,
    {
        fetch: function(request) {
            request.serverQuery = {q: request.query.name};
            return this.inherited("fetch", arguments);
        }
    }
);
var autocompleter;
<?php $this->dojo()->javascriptCaptureEnd();

// Once dijits have been created and all classes defined,
// instantiate the autocompleter and attach it to the element.
$this->dojo()->onLoadCaptureStart() ?>
function() {
    autocompleter = new custom.AutocompleteReadStore({
        url: "/test/autocomplete",
        requestMethod: "get"
    });
    dijit.byId("myAutoCompleteField").attr({
        store: autocompleter
    });
}
<?php $this->dojo()->onLoadCaptureEnd() ?>
<h1>Autocompletion Example</h1>
<div class="tundra">
<?php echo $this->form ?>
</div>
 

This works well, and is an expedient way to get autocompletion working for your element. However, it breaks the DRY principle as you cannot re-use the custom class in other areas. So, let's look at a better solution

Creating a reusable custom QueryReadStore class extension

The recommendation by the Dojo developers is that you should create this class as a javascript class, with your other javascript code. The reasons for this are numerous: you can re-use the class elsewhere, and you can also include it in custom builds -- which will ensure that it is stripped of whitespace and packed, leading to smaller downloads for your end users.

The process isn't as scary as it may initially sound. Assuming that your "public/" directory has the following structure:

public/
    js/
        dojo/
            dojo.js
        dijit/
        dojox/

what we'll do here is to create a sibling to the "dojo" subdirectory, called "custom", and create our class file there:

public/
    js/
        dojo/
            dojo.js
        dijit/
        dojox/
        custom/
            AutocompleteReadStore.js

We'll use the definition as originally shown above, and simply save it as "public/js/custom/AutocompleteReadStore.js", with one addition: after the dojo.provide call, add this:


dojo.require("dojox.data.QueryReadStore");
 

This is analagous to a require_once call in PHP, and ensures that the class has all dependencies prior to declaring itself. We'll leverage this fact later, when we hint in our ComboBox element what type of data store to use.

On the framework side of things, we're going to alter our element definition slightly to include information about the dojo.data store it will be using:


$form->addElement('ComboBox', 'myAutoCompleteField', array(
    'label'     => 'My autocomplete field:',

    // The javascript identifier for the data store:
    'storeId'   => 'autocompleter',

    // The class type for the data store:
    'storeType' => 'custom.AutocompleteReadStore',

    // Parameters to use when initializint the data store:
    'storeParams' => array(
        'url'           => '/foo/autocomplete',
        'requestMethod' => 'get',
    ),
));
 

If you've been following along closely, you'll notice that the "storeParams" are exactly the same as what we used to initialize the data store when inlining. The difference is that now the ComboBox view helper will create all the necessary Javascript for you.

The view script now becomes greatly simplified; we no longer need to setup any javascript, and can literally simply echo the form:


<?= $this->form ?>
 

Hopefully it should now be clear which method is easiest in the long run.

Next Steps

dojox.data.QueryReadStore offers much more than simply specifying the query string. As noted when introducing the component, it creates a JSON structure that also includes keys for sorting, selecting how many results to display, and offsets when pulling results. These, too, can be added to your query strings to allow finer grained selection of results -- for instance, you could ensure that no more than 3 or 5 results are returned, to allow for a more manageable list of matches, or specify a sort order that makes more sense to users.

Summary

Learning new tools can be difficult, and Dojo and Zend Framework are no exceptions. One compelling reason to learn Dojo if you're using Zend Framework, however, is that its structure and design should be familiar: it uses the same 1:1 class name:filename mapping paradigm. Additionally, because it is written to utilize strong OOP principles, familiar concepts such as extending classes can be used to customize Dojo for your site's needs.

Hopefully this tutorial will shed a little light on both the subject of autocompletion in Dojo, as well as class extensions in Dojo, and help get you started creating your own custom Dojo libraries for use with your applications.

Posted by Matthew Weier O'Phinney in Dojo, PHP at 11:07 | Comments (20) | Trackbacks (0)
Defined tags for this entry: best practices, dojo, php, zend framework
Related entries by tags:
Speaking at php|tek
Creating composite elements
Speaking at DPC (again!)
Rendering Zend_Form decorators individually
Zend Framework 1.8 PREVIEW Release

Trackbacks
Trackback specific URI for this entry

No Trackbacks

Comments
Display comments as (Linear | Threaded)

Thanks for the how to, Matthew. I have been wanting to write a similar post from a long time. I completed it today.

http://techchorus.net/autocomplete-example-zenddojoformelementfiltringselect-and-zenddojodata
#1 Sudheer (Link) on 2008-12-14 07:27 (Reply)
Well, before reading your article, I was wondering how Zend will integrate with Dojo...

After reading your nice article, I'm a bit disapointed as I thought that it will be no "javascript" code to do like mentionned in the capture part of your article...
#2 Nicolas BUI on 2008-12-15 03:37 (Reply)
Hi Nocolas,

It is possible to use Dojo widgets without writing a single line of JavaScript. I have written two articles on using Zend_Dojo without writing a single line of JavaScript.

But I think, as the application grows in size and complexity it requires custom JS coding. This article is a good starting point.
#2.1 Sudheer (Link) on 2008-12-15 04:23 (Reply)
@Nicoloas - as Sudheer noted, you *can* accomplish a lot with ZF's Dojo integration without writing any JS. In fact, if you use dojo.data.ItemFileReadStore for your dojo.data store, you can do autocompletes without writing any JS. The problem with that read store, however, is that you can't do any querying -- you get the _full_ list of potential matches. QueryReadStore, on the other hand, allows you to do selective querying -- i.e., if you typed "a", you can do lookups in your DB for 'LIKE 'a%'. This helps you limit the results presented to the user.

At a certain point, you simply _must_ start writing Javascript, and this tutorial was intended to show that it really does not need to be a scary thing. If you go the DRY route, you'll write a very simple, trivial JS file, drop it in your public tree, and you've suddenly got _very_ easy integration from ZF -- no JS on the ZF side whatsoever.
#2.2 Matthew Weier O'Phinney (Link) on 2008-12-15 06:49 (Reply)
Unfortunately I am not able to get this running. The searching and presenting the data by calling the controller action directly works as expected. The output also seems ok.

My AutocompleteReadStore.js looks like this:
-----------------------
dojo.provide("custom.AutocompleteReadStore");
dojo.require("dojox.data.QueryReadStore");
dojo.declare(
"custom.AutocompleteReadStore",
dojox.data.QueryReadStore,
{
fetch: function(request) {
request.serverQuery = {q: request.query.name};
return this.inherited("fetch", arguments);
}
}
);
-----------------------

My form looks like this
-----------------------
class Form_Autocomplete extends Zend_Dojo_Form
{
public function init()
{
$this->setMethod('post');
$this->setName('ajax_form');

$this->addElement('FilteringSelect', 'gericht', array(
'label' => 'Was möchtest du bestellen?',
'storeId' => 'autocompleter',
'storeType' => 'custom.AutocompleteReadStore',
'storeParams' => array(
'url' => '/index/autocomplete',
'requestMethod' => 'get',
),
));

$this->addElement('SubmitButton', 'submit_send', array(
'label' => 'Bestellen',
));
}
}
-----------------------

Whenever I enter anything in the input field I get the message "The value entered is not valid". The possible entries for my input are not shown. When I hit the tab to jump to the submit button, the message disappears and the number of the first entry matching my input is shown in the FilteringSelect field. This is weird!

When I use a ComboBox element, nothing seems to happen at all. I checked the http headers with the Live http headers browser plugin and the requests seem to be send. Maybe the output format of the response is wrong?

Any idea how to solve this?

Thanks, Ralf
#3 Ralf (Link) on 2009-01-09 15:03 (Reply)
To add some more information here is my view script:

----------------------------------------

----------------------------------------

And here some sample output from this view script

----------------------------------------
{"identifier":"dish_id","items":[{"dish_id":"3","dish_name":"Pizza Salami"},{"dish_id":"9","dish_name":"Spagetti Salami"},{"dish_id":"17","dish_name":"Calzone Salami"}],"label":"dish_name"}
----------------------------------------

When I delete the echo new Zend_Dojo_Data() line from my view script and enter the ouput string manually then the behavior of the ComboBox and FilteringSelect changes. When I enter anything I get a list of three options all called "undefined".

Could this be a combination of a wrong header and a format problem of the output or anything like this?
#3.1 Ralf (Link) on 2009-01-09 15:34 (Reply)
Ok, its me again, sorry the high posting frequency but I really want to get this done. I found a solution for the "undefined" problem. Dojo does not like any fields with an underscore, so I need to change the output like this:

----------------------------------------
{"identifier":"id","items":[{"id":"3","name":"Pizza Salami"},{"id":"9","name":"Spagetti Salami"},{"id":"17","name":"Calzone Salami"}],"label":"name"}
----------------------------------------

But still the output of the Zend_Dojo_Data in my view script does not work as expected. Here is my view-script:

----------------------------------------
$this->layout()->disableLayout();

$data = new Zend_Dojo_Data('id', $this->data, 'name');
echo $data;
----------------------------------------

When I delete the echo $data row and hardcode any output, it does work, but not when using Zend_Dojo_Data. How could this be?
#3.1.1 Ralf (Link) on 2009-01-09 15:43 (Reply)
Nice tutorial
i followed it but got the following error :
exception 'Zend_Loader_PluginLoader_Exception' with message 'Plugin by name 'ComboBox' was not found in the registry
can u attach the example files so we can download and take a look into it running plz ?
#4 Ahmed Abdel-Aliem (Link) on 2009-01-12 02:49 (Reply)
thanks for the article. I'm using a CDN for the Dojo sources. Using the example above throws the exception:

"uncaught exception: Could not load cross-domain resources: custom.AutocompleteReadStore"

How would I implement the example without losing the CDN option?
#5 Frank Quosdorf (Link) on 2009-01-12 23:23 (Reply)
hii friend please help me


i am using zend framework 1.7.1 and Dojo while running my code dojo gives error invalid controller
#6 mahendra on 2009-01-20 03:36 (Reply)
The proper place to ask support questions is on the ZF mailing lists.

Check to see if you are using the recommended apache RewriteRules -- we modified them for 1.6.0 as we discovered that they were too restrictive when developing Dojo applications -- and often led to Dojo resources being intercepted by the MVC layer.
#6.1 Matthew Weier O'Phinney (Link) on 2009-01-20 06:28 (Reply)
hiya - I've followed this tutorial and it's working well for me, so I must say thank you for that :-)

I'm stuck at what seems like the final hurdle: getting the widget to store the relevant value when I click on an entry in the dropdown list.

I've posted a more detailed question about it here: http://www.dojotoolkit.org/forum/dijit-dijit-0-9/dijit-support/autocomplete-filteringselect-problem-setting-value

Any help would be very much appreciated, cheers!
#7 dAN (Link) on 2009-02-19 10:46 (Reply)
Update: I got the widget to work by duplicating my "tradeName" db column as a new column "name", which confirms that the textbox is looking in the datastore for a value "name".

What I can't work out is how to tell the widget to use the value of "tradeName" instead. Weird that it works ok for the dropdown list...

Seems like a solution is close...
#7.1 dAN (Link) on 2009-02-20 07:52 (Reply)
When you create your dojo.data source, there are, minimally, three top level items: identifier, label, and items. Specify "tradeName" for the identifier property.

If using Zend_Dojo_Data to create your dojo.data payload, the identifier name is the first argument to the constructor: $data = new Zend_Dojo_Data("tradeName", $items);
#7.1.1 Matthew Weier O'Phinney (Link) on 2009-02-20 07:58 (Reply)
Thanks Matthew for taking the time to reply - yep had that already, but I got a reply over at dojotoolkit which helped me fix it.

When I create the widget I had to set the searchAttr property to "tradeName", and then I had to update my custom read store to pass the value of tradeName: request.serverQuery = {q: request.query.tradeName};

It means I can't re-use my custom read store as I'd like to but I'm relieved it's working, and this has given me a better insight into how all this stuff fits together.

Thanks again for your tutorial, all the best
dAN
#7.1.1.1 dAN (Link) on 2009-02-20 09:33 (Reply)
hi again Matthew

All is going well locally but when I upload my autocomplete form to the server (A2 hosting) where I get the following error message:

Fatal error: Uncaught exception 'Zend_Loader_PluginLoader_Exception' with message 'Plugin by name 'Textbox' was not found in the registry; used paths: Zend_Dojo_Form_Element_: Zend/Dojo/Form/Element/ Zend_Form_Element_: Zend/Form/Element/' in /usr/lib/php/ZendFramework/ZendFramework-1.7.2/library/Zend/Loader/PluginLoader.php:386 Stack trace: #0 /usr/lib/php/ZendFramework/ZendFramework-1.7.2/library/Zend/Form.php(1054): Zend_Loader_PluginLoader->load('textbox') #1 /usr/lib/php/ZendFramework/ZendFramework-1.7.2/library/Zend/Form.php(986): Zend_Form->createElement('textbox', 'peopleName', Array) #2 /home/sqrbrkt/zf/illhomes/application/forms/PeopleForm.php(15): Zend_Form->addElement('textbox', 'peopleName', Array) #3 /usr/lib/php/ZendFramework/ZendFramework-1.7.2/library/Zend/Form.php(223): Form_PeopleForm->init() #4 /usr/lib/php/ZendFramework/ZendFramework-1.7.2/library/Zend/Dojo/Form.php(50): Zend_Form->__construct(NULL) #5 /home/sqrbrkt/zf/illhomes/application/controllers/PeopleController.php(163): Zend_Dojo_Form->__const in /usr/lib/php/ZendFramework/ZendFramework-1.7.2/library/Zend/Loader/PluginLoader.php on line 386

This displays for any Zend_Dojo form that uses a textbox element, including my autocomplete forms.

A2 have the Zend framework preinstalled on the server - all I can think is that their version needs updating.

I'm following this with them but it's been a couple of days with no progress.

If you have any quick insights they be much appreciated

Cheers
dAN
#7.1.1.1.1 dAN (Link) on 2009-02-27 07:24 (Reply)
The plugin should be 'TextBox' or 'textBox' -- note the capital 'B'.
#7.1.1.1.1.1 Matthew Weier O'Phinney (Link) on 2009-02-27 10:15 (Reply)
I've just found that the reason I can't get this example working is down to a bug in ZF: http://framework.zend.com/issues/browse/ZF-4587.

Thought I'd let anyone else know who might be scratching their heads.
#8 SJD@Picture on 2009-02-25 11:05 (Reply)
yep that's done it :-)

I realised I'm running version 1.6.2 locally, but the server is running version 1.7.2 which I guess is why it was working fine locally.

Time to update! Cheers again
dAN
#9 dAN (Link) on 2009-02-27 11:22 (Reply)
...or more likely because I'm testing locally on windows (using WAMP) and the server is unix based
#9.1 dAN on 2009-03-02 08:35 (Reply)

Add Comment

Standard emoticons like :-) and ;-) are converted to images.
E-Mail addresses will not be displayed and will only be used for E-Mail notifications

To prevent automated Bots from commentspamming, please enter the string you see in the image below in the appropriate input box. Your comment will only be submitted if the strings match. Please ensure that your browser supports and accepts cookies, or your comment cannot be verified correctly.
CAPTCHA

 
 
  • Home
  • Resume
  • Blog
  • Phly PEAR Channel
  • Contact Me
  • About this site

ZCE

Zend Education Advisory Board Member

Add to Technorati Favorites

Calendar

Back July '09
Mon Tue Wed Thu Fri Sat Sun
    1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31    

Quicksearch

Links

  • PHLY - PHp LibrarY
  • Paul M. Jones
  • Mike Naberezny
  • Shahar Evron
  • Planet PHP
  • Zend Where I now work
  • Garden.org Where I once worked

Archives

July 2009
June 2009
May 2009
Recent...
Older...

Categories

XML Linux
XML Personal
XML Aikido
XML Family
XML Programming
XML Dojo
XML Perl
XML PHP

All categories

Syndicate This Blog

XML RSS 0.91 feed
XML RSS 1.0 feed
XML RSS 2.0 feed
ATOM/XML ATOM 0.3 feed
ATOM/XML ATOM 1.0 feed
XML RSS 2.0 Comments

Show tagged entries

xml best practices
xml books
xml conferences
xml decorators
xml dojo
xml dpc08
xml file_fortune
xml linux
xml mvc
xml oop
xml pear
xml perl
xml personal
xml php
xml phpworks08
xml programming
xml ubuntu
xml vim
xml webinar
xml zendcon
xml zendcon08
xml zend framework
© 2004 - present, Matthew Weier O'Phinney
matthew-web <at> weierophinney.net