TRAJOIN is an Application to Translate symfony documents Jointly.

home > 1.2/cookbook/en > make-a-choice.txt

[1] Edit ↑TOP

How to implement a choice in a form?


[2] Edit ↑TOP

When displaying a form, you often want the user to make a choice amongst a list of possibilities.


[3] Edit ↑TOP

In HTML, a choice is represented by a select tag:


[4] Edit ↑TOP

select tag


[5] Edit ↑TOP

You can add a multiple attribute to make it accept several choices:


[6] Edit ↑TOP

multiple select tag


[7] Edit ↑TOP

The sfWidgetFormChoice


[8] Edit ↑TOP

But a choice can also be represented by a list of radio button (single choice) or a list of checkboxes (multiple choices).


[9] Edit ↑TOP

To unify all these possibilities, symfony 1.2 comes with a new widget called sfWidgetFormChoice. sfWidgetFormChoice is an abstract widget in the sense that it delegates the rendering to another widget (the renderer widget).


[10] Edit ↑TOP

Let's take a simple example to illustrate all the possible combinations. In a project, we have the following schema:


[11] Edit ↑TOP

database schema


[12] Edit ↑TOP

// config/schema.yml
propel:
  demo_article:
    id:           ~
    author_id:    { type: integer, foreignReference: id, foreignTable: demo_author, onDelete: cascade, onUpdate: cascade, required: true }
    status:       varchar(255)
    title:        varchar(255)
    content:      longvarchar
    published_at: timestamp

  demo_category:
    id:          ~
    name:        varchar(255)

  demo_author:
    id:          ~
    name:        varchar(255)

  demo_tag:
    id:          ~
    name:        varchar(255)

  demo_tag_article:
    tag_id:      { type: integer, primaryKey: true, foreignReference: id, foreignTable: demo_tag, onDelete: cascade, onUpdate: cascade, required: true }
    article_id:  { type: integer, primaryKey: true, foreignReference: id, foreignTable: demo_article, onDelete: cascade, onUpdate: cascade, required: true }

  demo_category_article:
    category_id: { type: integer, primaryKey: true, foreignReference: id, foreignTable: demo_category, onDelete: cascade, onUpdate: cascade, required: true }
    article_id:  { type: integer, primaryKey: true, foreignReference: id, foreignTable: demo_article, onDelete: cascade, onUpdate: cascade, required: true }

[13] Edit ↑TOP

This is a classic schema for a simple CMS. Articles have an author and can have many tags and categories. Each article has also a status which value can be one of: published, draft, or deleted. The status value is stored as plain text as no table has been created to store the statuses.


[14] Edit ↑TOP

Let's play with the DemoArticle model by creating a module that provides the basic CRUD operations:


[15] Edit ↑TOP
$ php symfony propel:build-all
$ php symfony propel:generate-module frontend article DemoArticle

[16] Edit ↑TOP

If you navigate to the edit page, you will see something like this:


[17] Edit ↑TOP

raw form


[18] Edit ↑TOP

If you have a look at the generated form class for the DemoArticle model (lib/form/base/BaseDemoArticle.class.php), you will see that symfony uses sfWidgetFormPropelChoice for the author_id widget and sfWidgetFormPropelChoiceMany for the demo_category_article_list and demo_tag_article_list widgets. symfony has guessed the best widget to use based on the schema definition.


[19] Edit ↑TOP

sfWidgetFormPropelChoice represents a single choice widget based on a Propel object and sfWidgetFormPropelChoiceMany represents a multiple choice widget also based on a Propel object.


[20] Edit ↑TOP

Customizing the Form


[21] Edit ↑TOP

The first thing we can do to customize our form is to convert the status widget to a choice:


[22] Edit ↑TOP

form with a choice for the status


[23] Edit ↑TOP

First, we need to define the statuses in the DemoArticlePeer model class:


[24] Edit ↑TOP

// lib/model/DemoArticlePeer.php
class DemoArticlePeer extends BaseDemoArticlePeer
{
  static protected $choices = array(
    'published' => 'published',
    'draft'     => 'draft',
    'deleted'   => 'deleted'
  );

  static public function getStatusChoices()
  {
    return self::$choices;
  }
}

[25] Edit ↑TOP

Then, edit the DemoArticleForm class to change the widget and the validator associated with the status field:


[26] Edit ↑TOP

// lib/form/DemoArticleForm.class.php
class DemoArticleForm extends BaseDemoArticleForm
{
  public function configure()
  {
    $this->widgetSchema['status'] = new sfWidgetFormChoice(array(
      'choices' => DemoArticlePeer::getStatusChoices()
    ));

    $this->validatorSchema['status'] = new sfValidatorChoice(array(
      'choices' => array_keys(DemoArticlePeer::getStatusChoices())
    ));
  }
}

[27] Edit ↑TOP

The sfWidgetFormChoice takes an array of choices to use in the select tag as the choices option.


[28] Edit ↑TOP

The sfValidatorChoice also takes a choices option which is the valid values for the status column (the keys of the DemoArticlePeer::getStatusChoices() array).


[29] Edit ↑TOP

Playing with the Choices


[30] Edit ↑TOP

Radio button list


[31] Edit ↑TOP

Time to play a bit with the sfWidgetFormChoice widget! As you can see on the previous screenshot, the status is now represented by a select tag. But as the number of values for status is quite low, it would have been better to display the statuses as a list of radio buttons:


[32] Edit ↑TOP

form with a radio list for status


[33] Edit ↑TOP

That's quite easy to achieve. The sfWidgetFormChoice takes an expanded option that changes the output from a select tag to a list of radio buttons:


[34] Edit ↑TOP

$this->widgetSchema['status'] = new sfWidgetFormChoice(array(
  'choices'  => DemoArticlePeer::getStatusChoices(),
  'expanded' => true,
));

[35] Edit ↑TOP

Checkboxes list


[36] Edit ↑TOP

The list of categories is also quite small, so it would be better to display them as a list of checkboxes:


[37] Edit ↑TOP

form with a checkbox list for categories


[38] Edit ↑TOP

The expanded option we have used for single choices can also be used for multiple choice widgets. As the widget has been generated in the base form class and don't need to be changed, we can just set the expanded option to true:


[39] Edit ↑TOP

$this->widgetSchema['demo_category_article_list']->setOption('expanded', true);

[40] Edit ↑TOP

Summary


[41] Edit ↑TOP

The following table summarizes the different configuration of sfWidgetFormChoice and the renderer widget used by symfony:


[42] Edit ↑TOP
sfWidgetFormChoice expanded is false expanded is true
multiple is false sfWidgetFormSelect sfWidgetFormSelectRadio
multiple is true sfWidgetFormSelectMany sfWidgetFormSelectCheckbox

[43] Edit ↑TOP

The same table with some screenshots:


[44] Edit ↑TOP
sfWidgetFormChoice expanded is false expanded is true
multiple is false single not expanded single expanded
multiple is true multiple not expanded multiple expanded

[45] Edit ↑TOP

Group your Choices


[46] Edit ↑TOP

One of the less-known possibility of the select tag is the way you can group your choices with the optgroup feature:


[47] Edit ↑TOP

optgroup feature


[48] Edit ↑TOP

The sfWidgetFormChoice family widgets has built-in support for groups. You just need to pass an array of arrays for the choices options:


[49] Edit ↑TOP

$choices = array(
  'Europe'  => array('France' => 'France', 'Spain' => 'Spain', 'Italy' => 'Italy'),
  'America' => array('USA' => 'USA', 'Canada' => 'Canada', 'Brazil' => 'Brazil'),
);

$this->widgetSchema['country'] = new sfWidgetFormChoice(array('choices' => $choices));

[50] Edit ↑TOP

You can of course expand it to a list of radio buttons:


[51] Edit ↑TOP

$this->widgetSchema['country'] = new sfWidgetFormChoice(array(
  'choices'  => $choices,
  'expanded' => true,
));

[52] Edit ↑TOP

expanded optgroup feature


[53] Edit ↑TOP

You can also customize the layout used by the renderer widget:


[54] Edit ↑TOP

$this->widgetSchema['country'] = new sfWidgetFormChoice(array(
  'choices'  => $choices,
  'expanded' => true,
  'renderer_options' => array('template' => '<strong>%group%</strong> %options%'),
));

[55] Edit ↑TOP

expanded optgroup feature customized


[56] Edit ↑TOP

And yes, it also works with the multiple option:


[57] Edit ↑TOP

multiple optgroup feature


[58] Edit ↑TOP

multiple expanded optgroup feature


[59] Edit ↑TOP

More with JavaScript


[60] Edit ↑TOP

That was easy enough. Let's add some JavaScript to the mix to explore more possibilities.


[61] Edit ↑TOP

Double list


[62] Edit ↑TOP

If our CMS is used extensively, we will have more and more tags, and it will become more and more difficult to spot the tags associated with the current article. For such situations, a double list widget is one of the best solution:


[63] Edit ↑TOP

double list for tags


[64] Edit ↑TOP

Until now, symfony has chosen the best widget to use based on some simple configuration (multiple and expanded). But the sfWidgetFormChoice is not able to render our select tag widget as a double list out of the box.


[65] Edit ↑TOP

Luckily enough, we know that sfWidgetFormChoice delegates the rendering to another widget. Changing the rendering widget is as simple as modifying the renderer_class option.


[66] Edit ↑TOP

If you install the sfFormExtraPlugin, you will find a lot of interesting widgets and validators that are quite useful but did not make it into the core because they have third-party dependencies.


[67] Edit ↑TOP

The sfWidgetFormSelectDoubleList widget is one of them:


[68] Edit ↑TOP

$this->widgetSchema['demo_tag_article_list']->setOption('renderer_class', 'sfWidgetFormSelectDoubleList');

[69] Edit ↑TOP

If you refresh the page now, it won't work as the widget relies on some JavaScript to work correctly. The widget API documentation contains all you need to know to configure it properly:


[70] Edit ↑TOP

// apps/frontend/modules/article/templates/_form.php
<?php use_javascript('/sfFormExtraPlugin/js/double_list.js') ?>

<form action="<?php echo url_for('@article_update') ?>">
  <table>
    <?php echo $form ?>

    <!-- ... -->
  </table>
</form>

[71] Edit ↑TOP

Autocomplete


[72] Edit ↑TOP

We haven't played with the author_id field yet. Let's imagine that we have a lot of authors for our CMS, really a lot. It is not very easy to find something in a very long list of names in a drop-down select tag. So, let's convert this to an autocomplete widget.


[73] Edit ↑TOP

autocomplete for authors


[74] Edit ↑TOP

autocomplete for authors


[75] Edit ↑TOP

autocomplete for authors


[76] Edit ↑TOP

That's impressive, isn't it? To make it work, we will have to work a bit more than before.


[77] Edit ↑TOP

sfFormExtraPlugin contains two autocomplete widgets based on the JQuery library:


[78] Edit ↑TOP
  • sfWidgetFormJQueryAutocompleter: Can be used for any autocomplete task
  • sfWidgetFormPropelJQueryAutocompleter: Is optimized for Propel related autocompleter

[79] Edit ↑TOP

In our situation, we will use the Propel based one:


[80] Edit ↑TOP

// lib/form/DemoArticleForm.class.php
$this->widgetSchema['author_id']->setOption('renderer_class', 'sfWidgetFormPropelJQueryAutocompleter');
$this->widgetSchema['author_id']->setOption('renderer_options', array(
  'model' => 'DemoAuthor',
  'url'   => $this->getOption('url'),
));

[81] Edit ↑TOP

We have passed some options to the widget by setting renderer_options. In these options, you may have noticed the url is set to a url form option ($this->getOption('url')). When you create a form instance, the first constructor argument is the default values, and the second one is an array of options:


[82] Edit ↑TOP

public function executeEdit($request)
{
  // ...

  $this->form = new DemoArticleForm($article, array('url' => $this->getController()->genUrl('article/ajax')));

  // ...
}

[83] Edit ↑TOP

We now need to create the article/ajax action. When the widget calls this action, it passes several request parameters:


[84] Edit ↑TOP
  • q: The string entered by the user
  • limit: The maximum number of items to return

[85] Edit ↑TOP

Here is the code:


[86] Edit ↑TOP

// apps/frontend/modules/article/actions/actions.class.php
public function executeAjax($request)
{
  $this->getResponse()->setContentType('application/json');

  $authors = DemoAuthorPeer::retrieveForSelect($request->getParameter('q'), $request->getParameter('limit'));

  return $this->renderText(json_encode($authors));
}

// lib/model/DemoAuthorPeer.php
class DemoAuthorPeer extends BaseDemoAuthorPeer
{
  static public function retrieveForSelect($q, $limit)
  {
    $criteria = new Criteria();
    $criteria->add(DemoAuthorPeer::NAME, '%'.$q.'%', Criteria::LIKE);
    $criteria->addAscendingOrderByColumn(DemoAuthorPeer::NAME);
    $criteria->setLimit($limit);

    $authors = array();
    foreach (DemoAuthorPeer::doSelect($criteria) as $author)
    {
      $authors[$author->getId()] = (string) $author;
    }

    return $authors;
  }
}

[87] Edit ↑TOP

Now, as for every JavaScript widget, we also need to add some files to the form template to make it work properly:


[88] Edit ↑TOP

// apps/frontend/modules/article/templates/_form.php
<?php use_javascript('/sfFormExtraPlugin/js/jquery.autocompleter.js') ?>
<?php use_stylesheet('/sfFormExtraPlugin/css/jquery.autocompleter.css') ?>

<!-- ... -->

[89] Edit ↑TOP

We are done. We now have an autocomplete widget which is able to display the author names, and submit the author id to the form. And thanks to the validator, we are sure that only valid ids are submitted and saved to the database.


[90] Edit ↑TOP

Final Form


[91] Edit ↑TOP

Here is the final form which shows all the different way to ask the user for a choice:


[92] Edit ↑TOP

all possibilities


[93] Edit ↑TOP

That's a lot of flexibility for just one widget!


Comments

Menu

Documentation



Latest Histories

Untranslated