Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Form] Fix ChoiceType options #6393

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 114 additions & 41 deletions reference/forms/types/choice.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,14 @@ This will create a ``select`` drop-down like this:
.. image:: /images/reference/form/choice-example1.png
:align: center

If the user selects ``No``, the form will return ``false`` for this field. Similarly,
if the starting data for this field is ``true``, then ``Yes`` will be auto-selected.
In other words, the **value** of each item is the value you want to get/set in PHP
code, while the **key** is what will be shown to the user.
The model data of this field, the **choice** may be any of the ``choices`` option
values, while **keys** are used as default label that the user will see and select.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this sentence? I think it's explained better in the next paragraph.


If the starting data for this field is ``true``, then ``Yes`` will be auto-selected.
In other words, each value of the ``choices`` option is the **choice** data you
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.. each value (e.g. true) of the...

want to deal with in PHP code, while the **key** is the default label that will be
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

while the key (e.g. ``Yes) is...

shown to the user and the **value** is the string that will be submitted to the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a run-on sentence. How about this:

shown to the user. The **value** (e.g. ``true``) is also converted to a string and used as the "value"
HTML attribute when the field is rendered.

form and used in the template for the corresponding html attribute.

.. caution::

Expand All @@ -83,40 +87,98 @@ code, while the **key** is what will be shown to the user.
and will be removed in 3.0. To read about the old API, read an older version of
the docs.

.. note::

Pre selected choices will depend on the **data** passed to the field and
the values of the ``choices`` option. However submitted choices will depend
on the **string** matching the **choice**. In the example above, the default
values are incrementing integers because ``null`` cannot be casted to string.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

@yceruto yceruto Jan 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact, I think it's a bug ;) we can show "", "1", "0" values for array(null, true, false) choices and be more consistent.

Trying to fix that in symfony/symfony#21160

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As said in your PR you should close it, this is why this needs a better doc :)

You should consider it as well when dealing with ``empty_data`` option::

$builder->add('isAttending', 'choice', array(
'choices' => array(
'Maybe' => null,
'Yes' => true,
'No' => false,
),
'choices_as_values' => true,
'data' => true, // pre selected choice
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a bad practice. The reader could interpret that is a way to set default value and it isn't. As you know, if an object is bound to the form, the data option overrides this default value.

I suggest you to use $this->createFormBuilder(array('isAttending' => true)) from a controller for this sample. WDYT?

Copy link
Contributor Author

@HeahDude HeahDude Jan 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yceruto I understand your comment but this is not about a bad practice here, it's about making a clear difference as I'm not sure #6265 is enough though.

'empty_data' => '1', // default submitted value
));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we detail that when multiple it should be:

'data' => array(true),
'empty_data' => array('1'),

??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ref symfony/symfony#18817.

This clearly needs to explained.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Man, this note basically doesn't make any sense to me :). I don't think it's your fault - I just find this choice stuff really hard. What exactly is the problem that we're trying to warn the user about? The only issue I can understand is that empty_data (if you want to set that) would need to equal 1 to simulate true (I think, because apparently these values will render as 0, 1, 2 because they can't be cast into strings?).

I'm normally under the impression that the user will rarely care about the HTML value attribute that is rendered/submitted - as long as if I select "Yes", I end up with true in my code.


When the ``multiple`` option is ``true`` the submitted data is an array of
strings, you should the set the ``empty_value`` option accordingly.
Also note that as a scalar ``false`` data as string **value** is by default
``"0"`` to avoid conflict with placeholder value which is always an empty
string.

Advanced Example (with Objects!)
--------------------------------

This field has a *lot* of options and most control how the field is displayed. In
this example, the underlying data is some ``Category`` object that has a ``getName()``
method::

$builder->add('category', 'choice', [
'choices' => [
$builder->add('category', 'choice', array(
'choices' => array(
new Category('Cat1'),
new Category('Cat2'),
new Category('Cat3'),
new Category('Cat4'),
],
),
'choices_as_values' => true,
'choice_label' => function($category, $key, $index) {
/** @var Category $category */
'choice_label' => function(Category $category, $key, $value) {
return strtoupper($category->getName());
},
'choice_attr' => function($category, $key, $index) {
return ['class' => 'category_'.strtolower($category->getName())];
'choice_attr' => function(Category $category, $key, $value) {
return array('class' => 'category_'.strtolower($category->getName()));
},
'group_by' => function($category, $key, $index) {
'group_by' => function(Category $category, $key, $value) {
// randomly assign things into 2 groups
return rand(0, 1) == 1 ? 'Group A' : 'Group B';
},
'preferred_choices' => function($category, $key, $index) {
return $category->getName() == 'Cat2' || $category->getName() == 'Cat3';
'preferred_choices' => function(Category $category, $key, $value) {
return 'Cat2' === $category->getName() || 'Cat3' === $category->getName();
},
]);
));

You can also customize the `choice_name`_ and `choice_value`_ of each choice if
you need further HTML customization.

.. caution::

When dealing with objects as choices, you should be careful about how
string values are set to use them with the `empty_data` option.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... objects as choices, if you need to set the ``empty_data`` option, you may need to
override the ``choice_value``. In the example above, ...

In the example above, the default values are incrementing integers if the
``Category`` class does not implement ``toString`` method.
To get a full control of the string values use the `choice_value`_ option::
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To get full control...


$builder->add('category', 'choice', array(
'choices' => array(
new Category('Cat1'),
new Category('Cat2'),
new Category('Cat3'),
new Category('Cat4'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need indent

),
'choices_as_values' => true,
'choice_value' => function(Category $category = null) {
if (null === $category) {
return '';
}

return strtolower($category->getName());
},
'choice_label' => function(Category $category, $key, $value) {
return strtoupper($category->getName());
},
'multiple' => true,
'empty_data' => array('cat2'), // default submitted value
// an array because of multiple option
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a comment here about how this is matching what choice_value returns

));

Note that `choice_value`_ option set as a callable can get passed ``null``
when no data is preset or submitted.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This situation is only a problem if I need to control the empty_data option, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, anytime no value (null) is submitted and there is no placeholder (empty string).

.. _forms-reference-choice-tags:

.. include:: /reference/forms/types/options/select_how_rendered.rst.inc
Expand All @@ -135,19 +197,19 @@ Grouping Options

You can easily "group" options in a select by passing a multi-dimensional choices array::

$builder->add('stockStatus', 'choice', [
'choices' => [
'Main Statuses' => [
$builder->add('stockStatus', 'choice', array(
'choices' => array(
'Main Statuses' => array(
'Yes' => 'stock_yes',
'No' => 'stock_no',
],
'Out of Stock Statuses' => [
),
'Out of Stock Statuses' => array(
'Backordered' => 'stock_backordered',
'Discontinued' => 'stock_discontinued',
]
],
),
),
'choices_as_values' => true,
);
));

.. image:: /images/reference/form/choice-example4.png
:align: center
Expand All @@ -160,18 +222,25 @@ Field Options
choices
~~~~~~~

**type**: ``array`` **default**: ``array()``
**type**: ``array`` or ``\Traversable`` **default**: ``array()``

This is the most basic way to specify the choices that should be used
by this field. The ``choices`` option is an array, where the array key
is the item's label and the array value is the item's value::
is the choice's label and the array value is the choice's data::

$builder->add('inStock', 'choice', array(
'choices' => array('In Stock' => true, 'Out of Stock' => false),
'choices' => array(
'In Stock' => true,
'Out of Stock' => false,
),
// always include this
'choices_as_values' => true,
));

The component will try to cast the choices data to string to use it in view
format, in that case ``"0"`` and ``"1"``, but you can customize it using the
`choice_value`_ option.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The field will try to cast the choice values (e.g. ``true`` and ``false``) into strings to be rendered
in HTML (in this case, ``"0"`` and ``"1"```). But, you can customize these strings by using
the `choice_value`_ option.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will try is ambiguous. What if it fails? but you can customize When is this necessary?

This paragraph may be intrepreted as:

  • this component has a random behaviour (it will try but can fail)
  • customization is necessary for booleans (because the but part seems to be a solution to the previous sentence)


.. include:: /reference/forms/types/options/choice_attr.rst.inc

.. _reference-form-choice-label:
Expand Down Expand Up @@ -229,9 +298,14 @@ choice_loader

**type**: :class:`Symfony\\Component\\Form\\ChoiceList\\Loader\\ChoiceLoaderInterface`

The ``choice_loader`` can be used to only partially load the choices in cases where
a fully-loaded list is not necessary. This is only needed in advanced cases and
would replace the ``choices`` option.
The ``choice_loader`` can be used to load the choices form a data source with a
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... source with custom logic ...

custom logic (e.g query language) such as database or search engine.
The list will be fully loaded to display the form, but while submission only the
submitted choices will be loaded.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... display the form, but on submission, only the submitted choices will be loaded.


Also, the :class:``Symfony\\Component\\Form\\ChoiceList\\Factory\\ChoiceListFactoryInterface`` will cache the choice list
so the same :class:``Symfony\\Component\\Form\\ChoiceList\\Loader\\ChoiceLoaderInterface`` can be used in different fields with more performance
(reducing N queries to 1).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: a few of these lines got real long :)


.. include:: /reference/forms/types/options/choice_name.rst.inc

Expand All @@ -250,34 +324,33 @@ choices_as_values

The ``choices_as_values`` option was added to keep backward compatibility with the
*old* way of handling the ``choices`` option. When set to ``false`` (or omitted),
the choice keys are used as the underlying value and the choice values are shown
to the user.
the choice keys are used as the view value and the choice values are shown
to the user as label.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... as the label.


* Before 2.7 (and deprecated now)::

$builder->add('gender', 'choice', array(
// Shows "Male" to the user, returns "m" when selected
'choices' => array('m' => 'Male', 'f' => 'Female'),
$builder->add('agree', 'choice', array(
// Shows "Yes" to the user, returns "1" when selected
'choices' => array('1' => 'Yes', '0' => 'No'),
// before 2.7, this option didn't actually exist, but the
// behavior was equivalent to setting this to false in 2.7.
'choices_as_values' => false,
));

* Since 2.7::

$builder->add('gender', 'choice', array(
// Shows "Male" to the user, returns "m" when selected
'choices' => array('Male' => 'm', 'Female' => 'f'),
$builder->add('agree', 'choice', array(
// Shows "Yes" to the user, returns "1" when selected
'choices' => array('Yes' => '1', 'No' => '0'),
'choices_as_values' => true,
));

In Symfony 3.0, the ``choices_as_values`` option doesn't exist, but the ``choice``
type behaves as if it were set to true:
As of Symfony 3.0, the ``choices_as_values`` option is ``true`` by default:

* Default for 3.0::

$builder->add('gender', 'choice', array(
'choices' => array('Male' => 'm', 'Female' => 'f'),
$builder->add('agree', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', array(
'choices' => array('Yes' => '1', 'No' => '0'),
));

.. include:: /reference/forms/types/options/expanded.rst.inc
Expand Down
28 changes: 20 additions & 8 deletions reference/forms/types/options/choice_attr.rst.inc
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@ choice_attr
.. versionadded:: 2.7
The ``choice_attr`` option was introduced in Symfony 2.7.

**type**: ``array``, ``callable`` or ``string`` **default**: ``array()``
**type**: ``array``, ``callable``, ``string`` or :class:``Symfony\\Component\\PropertyAccess\\PropertyPath`` **default**: ``array()``

Use this to add additional HTML attributes to each choice. This can be an array
of attributes (if they are the same for each choice), a callable or a property path
(just like `choice_label`_).
Use this to add additional HTML attributes to each choice. This can be used as
a callable or a property path (just like `choice_label`_).
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this might be one reason why someone would end like symfony/symfony#17019


If an array, the keys of the ``choices`` array must be used as keys::
Also, if used as an array, the keys of the ``choices`` array must be used as keys::

$builder->add('attending', 'choice', array(
'choices' => array(
Expand All @@ -19,8 +18,21 @@ If an array, the keys of the ``choices`` array must be used as keys::
'Maybe' => null,
),
'choices_as_values' => true,
'choice_attr' => function($val, $key, $index) {
// adds a class like attending_yes, attending_no, etc
return ['class' => 'attending_'.strtolower($key)];
'choice_attr' => array(
// will be used for the second choice
'No' => array('class' => 'singular_choice_option');
),
));

$builder->add('attending', 'choice', array(
'choices' => array(
'Yes' => true,
'No' => false,
'Maybe' => null,
),
'choices_as_values' => true,
'choice_attr' => function() {
// will be used for all choices
return array('class' => 'choice_option');
},
));
54 changes: 33 additions & 21 deletions reference/forms/types/options/choice_label.rst.inc
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,29 @@ choice_label
.. versionadded:: 2.7
The ``choice_label`` option was introduced in Symfony 2.7.

**type**: ``string``, ``callable`` or ``false`` **default**: ``null``
**type**: ``string``, ``callable``, :class:``Symfony\\Component\\PropertyAccess\\PropertyPath`` or ``false`` **default**: ``null``

Normally, the array key of each item in the ``choices`` option is used as the
text that's shown to the user. The ``choice_label`` option allows you to take
more control::
more control.

If your choice values are objects (default in ``EntityType``), then ``choice_label``
can be a string :ref:`property path <reference-form-option-property-path>`. Imagine you have some
``Status`` class with a ``getDisplayName()`` method::

$builder->add('attending', 'choice', array(
'choices' => array(
new Status(Status::YES),
new Status(Status::NO),
new Status(Status::MAYBE),
),
'choices_as_values' => true,
'choice_label' => 'displayName',
));

If set as a callable is called for each choice, passing you its model data ``$choice``
and the ``$key`` from the choices array (default label). ``$value`` is either
`choice_value`_ option value when defined or the default string value of the choice::
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If set as a callable, your function is called for...

... from the choices array (the default label).

And I'm not sure we need that last sentence explaining $value.


$builder->add('attending', 'choice', array(
'choices' => array(
Expand All @@ -17,8 +35,8 @@ more control::
'maybe' => null,
),
'choices_as_values' => true,
'choice_label' => function ($value, $key, $index) {
if ($value == true) {
'choice_label' => function ($choice, $key, $value) {
if (true === $choice) {
return 'Definitely!';
}
return strtoupper($key);
Expand All @@ -28,26 +46,20 @@ more control::
},
));

This method is called for *each* choice, passing you the choice ``$value`` and the
``$key`` from the choices array (``$index`` is related to `choice_value`_). This
will give you:
The example above would output:

.. image:: /images/reference/form/choice-example2.png
:align: center

If your choice values are objects, then ``choice_label`` can also be a
:ref:`property path <reference-form-option-property-path>`. Imagine you have some
``Status`` class with a ``getDisplayName()`` method::

$builder->add('attending', 'choice', array(
'choices' => array(
new Status(Status::YES),
new Status(Status::NO),
new Status(Status::MAYBE),
),
'choices_as_values' => true,
'choice_label' => 'displayName',
));

If set to ``false``, all the tag labels will be discarded for radio or checkbox
inputs. You can also return ``false`` from the callable to discard certain labels.

.. caution::

If you want to pass a string property path wich is also a callable (e.g 'range'),
the component will treat it as a callable. You should pass a :class:``Symfony\\Component\\PropertyAccess\\PropertyPath``
object to ensure the expected behavior::

use Symfony\Component\PropertyAccess\PropertyPath;

'choice_label' => new PropertyPath('range'),
14 changes: 8 additions & 6 deletions reference/forms/types/options/choice_name.rst.inc
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ choice_name
.. versionadded:: 2.7
The ``choice_name`` option was introduced in Symfony 2.7.

**type**: ``callable`` or ``string`` **default**: ``null``
**type**: ``callable``, ``string`` or :class:``Symfony\\Component\\PropertyAccess\\PropertyPath`` **default**: ``null``

Controls the internal field name of the choice. You normally don't care about this,
but in some advanced cases, you might. For example, this "name" becomes the index
of the choice views in the template.
Controls the internal field name of the choice. You normally don't care about
this, but in some advanced cases, you might. For example, this "name" becomes
the index of the choice views in the template.

This can be a callable or a property path. See `choice_label`_ for similar usage.
If ``null`` is used, an incrementing integer is used as the name.
This can be a callable or a property path. Both needs to return a non empty
string, if ``null`` is used, an incrementing integer is used as the name.

See `choice_label`_ for similar usage.
Loading