Autofilling selectboxes aka ungrease your controllers!

By Simon Andreas Frimann Lund (safl)
Tired of populating the selectboxes in your form, from your controllers with:

$modelData = findAll(...)
$modelDataList = Set::combine(...)
$this->set('model_ids', $modelDataList);

I know i was!
How about not doing those tree lines of code for every model?
How about 0 lines of code in controller, and a simple property change on the selectbox? Yeah thats more like it!
This requires a generic function in the app_controller which I've called "listable" a helper function "isRequested" and override of the $formHelper->input() method.

I will start by showing you how to use this and afterwards you can grap the code and try it for yourself.

Usage


The usage is quite similar to the ajax->autocomplete(), you simply specify the controller function that will populate the selectbox, since we have made the generic "listable()" method all our controllers can be queried for data, and with pagination flavours one can populate in even more complex ways!

Start by adding the myForm helper to your controller $helpers = array(..., 'MyForm'); Then your can do stuff like the examples below.

The sample below populates a "Band" selectbox with all bands:

View Template:

Download code
<?= $myForm->input('Concert.band_id', array('options' => '/bands/listable') ?>

If your want, you can use "Pagination" arguments, below is an example of the selectbox populated with the 10 most popular bands.

View Template:

Download code
<?= $myForm->input('Concert.band_id', array('options' => '/bands/listable/order:popularity/limit:10') ?>

And you get this without doing any calls in your "Concert" controller!


Changes to a vanilla AppController


Place the code below in app/app_controller.php.

Controller Class:

Download code <?php 
class AppController extends Controller {

  
/**
   * isRequested()
   *
   * Determines whether the current action was requested, meaning if it was
   * executed by a "requestAction()" call.
   *
   */
  
function isRequested() {
    return isset(
$this->params['requested']);
  }

  
/**
   * listable(),
   *
   * @return array of id => name relation of the current model.
   * Takes pagination parameters, uber spiffness!
   *
   * TODO: Fix the crappy limit of 10000, it is there to avoid division by zero
   * but it ain't the way to do it....
   *
   */
  
function listable() {
    
    if (
$this->RequestHandler->isAjax()) {
            
$this->RequestHandler->renderAs($this'ajax');
    }
        
    
$modelClass $this->modelClass;
    
$primaryKey $this->{$modelClass}->primaryKey;
    
$prettyKey  = isset($this->params['named']['prettyKey']) ? $this->params['named']['prettyKey'] : 'name';
    
    
$this->paginate    = array($modelClass => 
                            array(
'limit'     => 10000,
                                  
'recursive' => -1,
                                  
'order'     => array("$modelClass.$prettyKey" => 'asc')
                            )
                      );
    
    
// query for data
    
$modelData $this->paginate($modelClass);
    
    
// requestAction?
    
if ($this->isRequested()) {
      if (empty(
$modelData)) {
        
$modelDataList = array();
      } else {
        
$modelDataList Set::combine($modelData"{n}.$modelClass.$primaryKey""{n}.$modelClass.$prettyKey");
      }
      return 
$modelDataList;
    } else {
      
$this->set(strtolower($modelClass).'_ids'$modelData);
    }
    
  }

}
?>

Extending formHelper


Place the code below in app/views/helpers/my_form.php.

Helper Class:

Download code <?php 
class MyFormHelper extends FormHelper {

  function 
input($fieldName$options=array()) {

    
// when options are string use the string as an url to populate the select options
    
if (isset($options['options']) && is_string($options['options'])) {    
      
$options['options'] = $this->requestAction($options['options']);        
    }
    
    return 
parent::input($fieldName$options);
    
  }

}
?>

Improvements


I'm well pleased with this solution but I would like to fix these issues:

  • It requires that you create your own formHelper (myHelper), it's ok for me since I've created and extended formhelper for all sorts of other purposes anyway, but it might not be for you.
  • The listable() function has a hardcoded limit of 10.000, this is a hack, and the function should be able to tell paginate in some other way that it want's all the entries....

Btw. you might think "why don't he just use elements and then reuse those?", well I started out with some nice reuse with elements and calling requestAction but then all my views got populated with $this->renderElement all over the place instead of the nice well known calls to a $formHelper.

I found extending the formHelper maintains the cake-way of things, since similar stuff has been done in $ajax->autocomplete().

I hope that you people will find this usefull and feedback to improvements are very very welcome!

 

Comments 739

CakePHP Team Comments Author Comments
 

Comment

1 Some notes

1. Why use paginate, rather than find('all')?

2. Since your Set::combine isn't using fancy stuff like combining multiple fields

PHP Snippet:

<?php Set::combine($posts'{n}.Post.id', array('%s %s''{n}.Post.title''{n}.Post.user_id'));?> why not just use find('list') ?

3. If they don't provide a 'prettyKey', perhaps it should fall back to $this->{$modelClass}->displayField, rather than hardcoded 'name'


However, personally I do find it more straightforward and understandable just to have

PHP Snippet:

<?php $band_list_options = array(
    
'conditions' => $band_conditions,
    
'fields' => array('Band.id''Band.pretty_field')
);
$this->set('bands'$this->Band->find('list'$band_list_options));?>

in my original controller. Sure it's a few lines, but then I know all my view data is being set in one place. Plus if you end up with a few requestActions you'll see a performance hit.
Posted Jul 17, 2008 by Grant Cox.