sfDoctrineSuperPagerPlugin - 0.9.1

An enhanced sfDoctrinePlugin, with AJAX, filtering form, column ordering and an AJAX form widget.

You are currently browsing
the website for symfony 1

Visit the Symfony2 website


« Back to the Plugins Home

Signin


Forgot your password?
Create an account

Tools

Stats

advanced search
Information Readme Releases Changelog Contribute
Show source

sfDoctrineSuperPagerPlugin

Overview

The sfDoctrineSuperPagerPlugin provides an enhanced sfDoctrinePager which allows you to:

  • Render a pager and have it become AJAX enabled with pagination, filtering and column ordering and still remain accessible without javascript enabled
  • Use an sfForm widget for an AJAX autocompletes/"search in a page" that can be used for foreign key model relations where there are too many records for a select dropdown.
  • Create pager objects which have enough information to render themselves, making reusing your pagers easier

Screenshots

Here's the pager sorted by a column:

pager

Here's the autocomplete... searching for some soap ;)

autocomplete

Requirements

  • sfForm, so probably symfony 1.1
  • sfDoctrinePlugin
  • Prototype 1.6 (I haven't tried with 1.5 for a while) (important - see info at the end of this file about a bug in 1.6.0.2)

Installation

  • Install the plugin

    Command line:

    symfony plugin-install http://plugins.symfony-project.com/sfDoctrineSuperPagerPlugin
    

    Or, you can use SVN. In this case you will have to copy the contents of the plugin's web/ directory to a sfDoctrineSuperPagerPlugin/ directory in your project's web folder.

  • Clear your cache

    symfony cc
    

Why super pager?!

Of course, there are a million other grid views and autocompletes out there, why bother with this one?

Well, the super pager is very fast, as it doesn't do anything very complicated and is really just a way of making simple paginated result sets work in a faster way. It has been my experience of the larger grid tools like YUI that they tend to be slow, but then again they are also a lot more flexible.

Even if you don't use the ajax features (eg. by not calling the super_pager_javascript helper), it's still nice to be able to create a pager class which has the extra information (form and columns) which can be reused around the site.

TAs for the automcomplete, I have been unhappy with all of the autocompletes I have tried before, but then this one works more like a "search in a page" with pagination. I have used this in the backends of many applications and it is a really nice way to make selectors for foreign key relations.

The super pager

A super pager sfDoctrineSuperPager object is like a usual sfDoctrinePager with the following additions:

  • Column definitions
  • A filter/search form
  • Row rendering capability

Once you have defined a super pager you can reuse it in multiple places with no duplication of rendering code. When rendered (which can be done by passing the pager to one function), the pager is put into the page in usual HTML. If the client does not have javascript then the pager can be used like normal. If the client does have javascript, the filter form and pagination are taken over and all of the pager communication is done over ajax. This works particulary nicely if you have two pagers on one page, which would be a pain otherwise. The column headers also become clickable to order the results by each column.

Bear in mind the following:

  • In the AJAX version the history is broken. I have had the pager working with a history manage (see todo at end) but this is not documented/well tested at the moment

Example super pager usage

There is an example module inside the plugin which shows how I have been using the super pager, that's where the example here is from.

  • This example is for a Product model, which has an id column and a name column. We want to make a pager which lists out all of the products along with a form to filter the pager results down to find specific products.

  • First, we make a filter form:

    class ExampleForm extends sfForm {
        public function configure() {
            $this->widgetSchema = new sfWidgetFormSchema(array(
                'id' => new sfWidgetFormInput(),
                'name' => new sfWidgetFormInput(),
            ));
    
            $this->validatorSchema = new sfValidatorSchema(array(
                'id' => new sfValidatorString(array('required' => false)),
                'name' => new sfValidatorString(array('required' => false))
            ));
    
            $this->widgetSchema->setNameFormat('filter[%s]');       
        }
    }
    

    Our filter form has a field for name and a field for id. For filter forms we generally don't want required fields.

  • Now, define our pager:

    class ExamplePager extends sfDoctrineSuperPager {
        public function __construct() {
            $form = new ExampleForm();
    
            $cols = array(
                0 => array(
                    'name' => 'Id',
                    'order' => 'id'
                ),
                1 => array(
                    'name' => 'Name',
                    'order' => 'name'
                ),
                2 => array(
                    'name' => 'Edit'
                )
            );
    
            parent::__construct('Product', $form, null, $cols);
        }
    
        public function addFilterValuesToQuery($request) {  
            $this->filterForm->bind($request->getParameter('filter'));
    
            if ($this->filterForm->isValid()) {
                if ($name = $this->filterForm->getValue('name')) {
                    $q = $this->getQuery();
                    $q->addWhere("name LIKE ?", array("%$name%"));
                }
                if ($id = $this->filterForm->getValue('id')) {
                    $q = $this->getQuery();
                    $q->addWhere("id >= ?", array($id));
                }
            }
        }
    
        public function renderRow($product) {
            return array(
                array(esc_entities($product['id'])),
                array(esc_entities($product['name'])),
                array(link_to('Edit', 'pagerExamples/index'))
            );
        }
    }
    

    This has three main parts. First, the column definitions inside the constructor:

    $cols = array(
        0 => array(
            'name' => 'Id',
            'order' => 'id'
        ),
        1 => array(
            'name' => 'Name',
            'order' => 'name'
        ),
        2 => array(
            'name' => 'Edit'
        )
    );
    

    This tells our pager we have three columns, called "Id", "Name" and "Edit". It also gives DQL ordering for the Name and id cols.

    Next, we have the part which tells our pager how to handle the filter form:

    public function addFilterValuesToQuery($request) {  
        $this->filterForm->bind($request->getParameter('filter'));
    
        if ($this->filterForm->isValid()) {
            if ($name = $this->filterForm->getValue('name')) {
                $q = $this->getQuery();
                $q->addWhere("name LIKE ?", array("%$name%"));
            }
            if ($id = $this->filterForm->getValue('id')) {
                $q = $this->getQuery();
                $q->addWhere("id >= ?", array($id));
            }
        }
    }
    

    If our form is valid, we add the values into the pager's query to restrict the items returned.

    Thirdly, we tell our pager how to render each row:

    public function renderRow($product) {
        return array(
            array(esc_entities($product['id'])),
            array(esc_entities($product['name'])),
            array(link_to('Edit', 'pagerExamples/index'))
        );
    }
    

    This renderRow method is always called inside the view so you can use your usual helper functions.

    NB - If having this here makes you feel uneasy about MVC seperation, the sfDoctrineSuperPager version of this method actually just asks for a helper function to use :)

  • We've got our pager, so let's use it in an action:

    /**
     * Page with a super pager on
     */
    public function executePager() {
        $pager = new ExamplePager();
        $pager->initFromRequest($this->getRequest());
    
        $this->pager = $pager;
    }
    

    Inside the template for this action we render the pager:

    use_helper('sfDoctrineSuperPager');
    
    echo super_pager_render(
        $pager, 
        $pagerUrl, 
        $pagerSourceUrl
    );
    

    Here, the $pagerUrl is the url of the current page, so the non-javascript pagination can work still. The $pagerSourceUrl... well, let's do that now.

  • If we view that page we will see a shiny super pager. But, the AJAX paging, ordering and searching will not work yet, because we need to somehow provide our pager with a way to get this information. We have to define another action:

    public function executePagerSource($request) {
        $pager = new ExamplePager();
        $pager->initFromRequest($request);
    
        $this->pager = $pager;
    
        sfSuperPagerTools::setResultsTemplateForAction($this);
    }
    

    This action looks a lot like our last one! The setResultsTemplateForAction sets the template of our action to be one from the plugin which returns pager results as JSON.

  • That's it! Our pager should be working.

The autocomplete

"Autocomplete" is not the best name for this tool, it's more of a search-in-a-page. It's good for admin backends as a way to select a model for a foreign key relationship when there are too many for a select. It has the following features:

  • As you type, the list of items gets narrowed down
  • Keyboard commands work, so you can use up, down, left, right and esc to navigate through the pager
  • Results are paginated
  • Results are cached in the client
  • Stale results are ignored - ie. if one server request is delayed, it will not mess up your search

Of course, this doesn't work without javascript, which is why I have been using it in backends really.

Autocomplete example

Here's another example taken from the module that comes with the plugin. We'll create a form which can be used to select a product_id for some purpose.

  • First, make a super pager which will act as a source for our field:

    class AutocompleteExamplePager extends sfDoctrineSuperPager {
        public function __construct() {     
            parent::__construct('Product', null);
        }
    
    
        /**
         * From the autocomplete we get one request parameter, "filter"
         *
         * @param sfRequest $request
         */
        public function addFilterValuesToQuery($request) {  
            if ($filterValue = $request->getParameter('filter')) {
                $q = $this->getQuery();
                $q->addWhere("name LIKE ?", array("%$filterValue%"));
            }
        }
    
    
        /**
         * There is a special format for the row rendering for autocomplete items:
         * 
         * array(
         *     array(itemId),
         *     array(item as HTML)
         * )
         * 
         *
         * @param unknown_type $product
         * @return unknown
         */
        public function renderRow($product) {
            return array(
                array(esc_entities($product['id'])),
                array(esc_entities($product['name']))
            );
        }
    }
    
    • Now, create our form in the action. Here I'm being a bit lazy and not creating a seperate form class:

      $pager = new AutocompleteExamplePager();

      // make a form with our autocomplete widget in $form = new sfForm(); $form->setWidgetSchema(new sfWidgetFormSchema(array( 'product_id' => new sfWidgetFormDoctrineSelectAjax( array( 'sfDoctrineSuperPager' => $pager, 'sourceUrl' => $this->getModuleName() . '/autocompleteAjax' ) ) ))); $form->setValidatorSchema(new sfValidatorSchema(array( 'product_id' => new sfValidatorDoctrineQuery(array( 'validateQuery' => $pager )) ))); $form->getWidgetSchema()->setNameFormat('example[%s]');

    The key part of this is the use of the sfWidgetFormDoctrineSelectAjax widget. We have one more thing to do before this form can be used... define a source action to provide our autocomplete with data:

    public function executeAutocompleteAjax($request) { 
        $pager = new AutocompleteExamplePager();
        $pager->initFromRequest($this->getRequest());   
    
        $this->pager = $pager;
        sfSuperPagerTools::setResultsTemplateForAction($this);
    }
    
    • That's it - we now have an autocomplete for a product. Of course, we only have to define the source action once inside our app, we can reuse it for each product autocomplete form widget we create.

Plugin contents

data/AjaxResultsSuccess.php                                        
    Template for an AJAX action which acts as a json data source for a pager
lib/helper/sfDoctrineSuperPagerHelper.php                                
    The helper used for rendering etc.
lib/sfDoctrineSuperPager.class.php               
    The pager itself
lib/sfValidatorDoctrineQuery.class.php           
    An sfForm validator to validate a model item id against a query.
lib/sfWidgetFormDoctrineSelectAjax.class.php     
    An sfForm widget for the javascript autocomplete field
web/*                                            
    The javascript, css and images used by the plugin

Prototype woes

There is a bug in Prototype 1.6.0.2 which breaks Element.clonePosition inside of ie, details are here: http://dev.rubyonrails.org/ticket/11473. The bug does not affect 1.6.0 as far as I am aware.

This functionality is required for the autocomplete to work, so I have included a patched version of prototype 1.6.0.2. By default this is being included by the super_pager_init_js() function. There is a config setting in there for you to stop it if you want to.

Todo

  • In some installations I have the super pager working with the RSH history manager (http://code.google.com/p/reallysimplehistory/) so the back button still works. The code is in there but not very well tested at the moment.
  • Maybe convert to the Doctrine built in pager?
  • Improve error display for pager