8000 [Form] Allow additional HTML attributes to be specified for choices · Issue #3836 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[Form] Allow additional HTML attributes to be specified for choices #3836

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

Closed
webmozart opened this issue Apr 8, 2012 · 18 comments
Closed

[Form] Allow additional HTML attributes to be specified for choices #3836

webmozart opened this issue Apr 8, 2012 · 18 comments

Comments

@webmozart
Copy link
Contributor

It would be nice to be able to define extra HTML attributes for choices in a choice list that are added to the option or input tags.

Origin of this ticket: #3456

@webda2l
Copy link
webda2l commented May 25, 2012
<option disabled = "disabled" />

will be useful as minimal add.
http://www.w3.org/TR/html-markup/option.html

@Tobion
Copy link
Contributor
Tobion commented Nov 27, 2012

same as #2754
@bschussek: But in that ticket, you said it won't be supported. So apparently you changed your mind :)

@webmozart
Copy link
Contributor Author

@Tobion This feature is being requested again and again, so I want to check again if there isn't some way to support this.

@raphox
Copy link
raphox commented Jan 15, 2013

maybe this helps you: #3835 (comment)

@ryanotella
Copy link

Non-specific Implementation

Only for 'expanded' => false. Not sure if this could be rolled into a meaningful PR. I'd have a go if there was any interest.

Example

$builder
        ->add(
                'incident',
                'extended_entity',
                array(
                    'label' => 'Incident',
                    'class' => 'Vendor\YourBundle\Entity\Incident',
                    'required' => true,
                    // 'option_attributes' injects $incident->getSlug() into each option tag e.g. <option data-slug="foo"...> 
                    'option_attributes' => array('data-slug' => 'slug'), 
                )
            );

Form Type

class ExtendedEntityType extends AbstractType
{
    /**
     * @var PropertyAccessorInterface
     */
    private $propertyAccessor;

    /**
     * @param PropertyAccessorInterface $propertyAccessor
     */
    function __construct(PropertyAccessorInterface $propertyAccessor)
    {
        $this->propertyAccessor = $propertyAccessor;
    }

    /**
     * @param FormView      $view
     * @param FormInterface $form
     * @param array         $options
     */
    public function finishView(FormView $view, FormInterface $form, array $options)
    {
        parent::finishView($view, $form, $options);

        foreach ($view->vars['choices'] as $choice) {
            $additionalAttributes = array();
            foreach ($options['option_attributes'] as $attributeName => $choicePath) {
                $additionalAttributes[$attributeName] = $this->propertyAccessor->getValue($choice->data, $choicePath);
            }

            $choice->attr = array_replace(isset($choice->attr) ? $choice->attr : array(), $additionalAttributes);
        }
    }

    /**
     * @param OptionsResolverInterface $resolver
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        parent::setDefaultOptions($resolver);

        $resolver->setDefaults(
            array(
                'option_attributes' => array(),
            )
        );
    }

    /**
     * @return string
     */
    public function getParent()
    {
        return 'entity';
    }

    /**
     * Returns the name of this type.
     *
     * @return string The name of this type
     */
    public function getName()
    {
        return 'extended_entity';
    }
}

Service

services:
    vendor.extended_entity.widget:
        class: Vendor\YourBundle\Form\ExtendedEntityType
        arguments: [@property_accessor]
        tags:
            - { name: form.type, alias: extended_entity }

Form Customization

{% block choice_widget_options %}
{% spaceless %}
    {% for group_label, choice in options %}
        {% if choice is iterable %}
            <optgroup label="{{ group_label|trans({}, translation_domain) }}">
                {% set options = choice %}
                {{ block('choice_widget_options') }}
            </optgroup>
        {% else %}
            <option {% for attrname, attrvalue in choice.attr|default({}) if attrvalue is not empty %}{{ attrname }}="{{ attrvalue }}" {% endfor %} value="{{ choice.value }}"{% if choice is selectedchoice(value) %} selected="selected"{% endif %}>{{ choice.label|trans({}, translation_domain) }}</option>
        {% endif %}
    {% endfor %}
{% endspaceless %}
{% endblock choice_widget_options %}

@gagarine
Copy link

#issuecomment-23145270 works nicely! the only things is the attributes are lowercased.

@gagarine
Copy link

If you want the same with radio

    public function finishView(FormView $view, FormInterface $form, array $options)
    {
        parent::finishView($view, $form, $options);

        foreach ($view->vars['choices'] as $choice) {
            $additionalAttributes = array();
            foreach ($options['option_attributes'] as $attributeName => $choicePath) {
                $additionalAttributes[$attributeName] = $this->propertyAccessor->getValue($choice->data, $choicePath);
            }

            $choice->attr = array_replace(isset($choice->attr) ? $choice->attr : array(), $additionalAttributes);
        }

        if ($options['expanded']) {
            foreach ($view as $childView) {
                $additionalAttributes = array();
                foreach ($options['option_attributes'] as $attributeName => $choicePath) {
                    $entityID = $childView->vars['value'];
                    $entity = $view->vars['choices'][$entityID]->data;
                    $additionalAttributes[$attributeName] = $this->propertyAccessor->getValue($entity, $choicePath);
                }

                $childView->vars['attr'] = array_replace(isset($childView->attr) ? $childView->attr : array(), $additionalAttributes);
            }
        }

    }

And the template

{% block radio_widget %}
    {% spaceless %}
        <input type="radio" {% for attrname, attrvalue in choice.attr|default({}) if attrvalue is not empty %}{{ attrname }}="{{ attrvalue }}" {% endfor %} {{ block('widget_attributes') }}{% if value is defined %} value="{{ value }}"{% endif %}{% if checked %} checked="checked"{% endif %} />
    {% endspaceless %}
{% endblock radio_widget %}

Because the methode addSubForms of Symfony\Component\Form\Extension\Core\Type\ChoiceType is priv 8000 ate...

@webmozart
Copy link
Contributor Author

Closed in favor of #9177

@webmozart
Copy link
Contributor Author

Bogus. This does not only apply to child fields, but also to <option> tags. Reopening.

@webmozart webmozart reopened this Sep 30, 2013
@thomasbennett
Copy link

It looks like this requires using a class, which throws an error if not.

@ryanotella
Copy link

Possibly a closure would be a more flexible option. Although the whole idea doesn't quite feel right. Maybe it would be better if option/radio/checkbox attributes were themeable in a consistent way.

On 16 Oct 2013, at 3:48 am, Thomas Bennett notifications@github.com wrote:

It looks like this requires using a class, which throws an error if not.


Reply to this email directly or view it on GitHub.

@keyboardSmasher
Copy link

Thanks for the code @ryancastle and @gagarine!

@vinhtq
Copy link
vinhtq commented Jan 9, 2014

Quite complicated for simple thing!

@keyboardSmasher
Copy link

@vinhtq Yeah, it is. But a "symfony" is not one person crashing cymbals together. :P

8000

@ryanotella
Copy link

Totally. That's the Symfony form component. Making the complicated easy and the easy complicated.

@webmozart
Copy link
Contributor Author

Closing in favor of #4067.

@jmauerhan
Copy link

@ryancastle thank you for the perfect code. I was able to get this working in just 2 tries for my custom choice list I already had written.

@inakiarroyo
Copy link

@ryancastle thank you, run perfect!

webmozart added a commit that referenced this issue Apr 1, 2015
…l, value, index and attribute generation (webmozart)

This PR was merged into the 2.7 branch.

Discussion
----------

[Form] Refactored choice lists to support dynamic label, value, index and attribute generation

This is a rebase of #12148 on the 2.7 branch.

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | yes
| Deprecations? | yes
| Tests pass?   | yes
| Fixed tickets | #4067, #5494, #3836, #8658, #12148
| License       | MIT
| Doc PR        | TODO

I implemented the additional options "choice_label", "choice_name", "choice_value", "choice_attr", "group_by" and "choices_as_values" for ChoiceType. Additionally the "preferred_choices" option was updated to accept callables and property paths.

The "choices_as_values" option will be removed in Symfony 3.0, where the choices will be passed in the values of the "choices" option by default. The reason for that is that, right now, choices are limited to strings and integers (i.e. valid array keys). When we flip the array, we remove that limitation. Since choice labels are always strings, we can also always use them as array keys:

```php
// Not possible currently, but possible with "flip_choices"
$builder->add('attending', 'choice', array(
    'choices' => array(
        'Yes' => true,
        'No' => false,
        'Maybe' => null,
    ),
    'choices_as_values' => true,
));
```

All the features described here obviously also apply to subtypes of "choice", such as "entity".

**choice_label**

Returns the label for each choice. Can be a callable (which receives the choice as first and the key of the "choices" array as second argument) or a property path.

If `null`, the keys of the "choices" array are used as labels.

```php
// callable
$builder->add('attending', 'choice', array(
    'choices' => array(
        'yes' => true,
        'no' => false,
        'maybe' => null,
    ),
    'choices_as_values' => true,
    'choice_label' => function ($choice, $key) {
        return 'form.choice.'.$key;
    },
));

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

**choice_name**

Returns the form name for each choice. That name is used as name of the checkbox/radio form for this choice. It is also used as index of the choice views in the template. Can be a callable (like for "choice_label") or a property path.

The generated names must be valid form names, i.e. contain alpha-numeric symbols, underscores, hyphens and colons only. They must start with an alpha-numeric symbol or an underscore.

If `null`, an incrementing integer is used as name.

```php
// callable
$builder->add('attending', 'choice', array(
    'choices' => array(
        'Yes' => true,
        'No' => false,
        'Maybe' => null,
    ),
    'choices_as_values' => true,
    'choice_name' => function ($choice, $key) {
        // use the labels as names
        return strtolower($key);
    },
));

// property path
$builder->add('attending', 'choice', array(
    'choices' => array(
        'Yes' => Status::getInstance(Status::YES),
        'No' => Status::getInstance(Status::NO),
        'Maybe' => Status::getInstance(Status::MAYBE),
    ),
    'choices_as_values' => true,
    'choice_name' => 'value',
));
```

**choice_value**

Returns the string value for each choice. This value is displayed in the "value" attributes and submitted in the POST/PUT requests. Can be a callable (like for "choice_label") or a property path.

If `null`, an incrementing integer is used as value.

```php
// callable
$builder->add('attending', 'choice', array(
    'choices' => array(
        'Yes' => true,
        'No' => false,
        'Maybe' => null,
    ),
    'choices_as_values' => true,
    'choice_value' => function ($choice, $key) {
        if (null === $choice) {
            return 'null';
        }

        if (true === $choice) {
            return 'true';
        }

        return 'false';
    },
));

// property path
$builder->add('attending', 'choice', array(
    'choices' => array(
        'Yes' => Status::getInstance(Status::YES),
        'No' => Status::getInstance(Status::NO),
        'Maybe' => Status::getInstance(Status::MAYBE),
    ),
    'choices_as_values' => true,
    'choice_value' => 'value',
));
```

**choice_attr**

Returns the additional HTML attributes for choices. Can be an array, a callable (like for "choice_label") or a property path.
If an array, the key of the "choices" array must be used as keys.

```php
// array
$builder->add('attending', 'choice', array(
    'choices' => array(
        'Yes' => true,
        'No' => false,
        'Maybe' => null,
    ),
    'choices_as_values' => true,
    'choice_attr' => array(
        'Maybe' => array('class' => 'greyed-out'),
    ),
));

// callable
$builder->add('attending', 'choice', array(
    'choices' => array(
        'Yes' => true,
        'No' => false,
        'Maybe' => null,
    ),
    'choices_as_values' => true,
    'choice_attr' => function ($choice, $key) {
        if (null === $choice) {
            return array('class' => 'greyed-out');
        }
    },
));

// property path
$builder->add('attending', 'choice', array(
    'choices' => array(
        'Yes' => Status::getInstance(Status::YES),
        'No' => Status::getInstance(Status::NO),
        'Maybe' => Status::getInstance(Status::MAYBE),
    ),
    'choices_as_values' => true,
    'choice_value' => 'htmlAttributes',
));
```

**group_by**

Returns the grouping used for the choices. Can be an array/Traversable, a callable (like for "choice_label") or a property path.

The return values of the callable/property path are used as group labels. If `null` is returned, a choice is not grouped.

If `null`, the structure of the "choices" array is used to construct the groups.

```php
// default
$builder->add('attending', 'choice', array(
    'choices' => array(
        'Decided' => array(
            'Yes' => true,
            'No' => false,
        ),
        'Undecided' => array(
            'Maybe' => null,
        ),
    ),
    'choices_as_values' => true,
));

// callable
$builder->add('attending', 'choice', array(
    'choices' => array(
        'Yes' => true,
        'No' => false,
        'Maybe' => null,
    ),
    'choices_as_values' => true,
    'group_by' => function ($choice, $key) {
        if (null === $choice) {
            return 'Undecided';
        }

        return 'Decided';
    },
));

// property path
$builder->add('attending', 'choice', array(
    'choices' => array(
        'Yes' => Status::getInstance(Status::YES),
        'No' => Status::getInstance(Status::NO),
        'Maybe' => Status::getInstance(Status::MAYBE),
    ),
    'choices_as_values' => true,
    'group_by' => 'type',
));
```

**preferred_choices**

Returns the preferred choices. Can be an array/Traversable, a callable (like for "choice_label") or a property path.

```php
// array
$builder->add('attending', 'choice', array(
    'choices' => array(
        'Yes' => true,
        'No' => false,
        'Maybe' => null,
    ),
    'choices_as_values' => true,
    'preferred_choices' => array(true),
));

// callable
$builder->add('attending', 'choice', array(
    'choices' => array(
        'Yes' => true,
        'No' => false,
        'Maybe' => null,
    ),
    'choices_as_values' => true,
    'preferred_choices' => function ($choice, $key) {
        return true === $choice;
    },
));

// property path
$builder->add('attending', 'choice', array(
    'choices' => array(
        'Yes' => Status::getInstance(Status::YES),
        'No' => Status::getInstance(Status::NO),
        'Maybe' => Status::getInstance(Status::MAYBE),
    ),
    'choices_as_values' => true,
    'preferred_choices' => 'preferred',
));
```

**Technical Changes**

To properly implement all this, the old `ChoiceListInterface` class was deprecated and replaced by a new, slimmer one. The creation of choice views is now separated from choice lists. Hence a lot of logic is not executed anymore when processing (but not displaying) a form.

Internally, a `ChoiceListFactoryInterface` implementation is used to construct choice lists and choice views. Two decorators exist for this class:

* `CachingFactoryDecorator`: caches choice lists/views so that multiple fields displaying the same choices (e.g. in collection fields) use the same choice list/view
* `PropertyAccessDecorator`: adds support for property paths to a factory

**BC Breaks**

The option "choice_list" of ChoiceType now contains a `Symfony\Component\Form\ChoiceList\ChoiceListInterface` instance, which is a super-type of the deprecated `ChoiceListInterface`.

**Todos**

- [ ] Adapt CHANGELOGs
- [ ] Adapt UPGRADE files
- [ ] symfony/symfony-docs issue/PR

Commits
-------

94d18e9 [Form] Fixed CS
7e0960d [Form] Fixed failing layout tests
1d89922 [Form] Fixed tests using legacy functionality
d6179c8 [Form] Fixed PR comments
26eba76 [Form] Fixed regression: Choices are compared by their values if a value callback is given
a289deb [Form] Fixed new ArrayChoiceList to compare choices by their values, if enabled
e6739bf [DoctrineBridge] DoctrineType now respects the "query_builder" option when caching the choice loader
3846b37 [DoctrineBridge] Fixed: don't cache choice lists if query builders are constructed dynamically
03efce1 [Form] Refactored choice lists to support dynamic label, value, index and attribute generation
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

0