Skip to content

Commit 104f542

Browse files
committed
[Form] add choice_label_attr option to ChoiceType
BC break: change `ChoiceListFactoryInterface`. Dynamicaly set the label html attributes for each choice. Same use as `choice_attr` option, only if `expanded`.
1 parent 6fb9fee commit 104f542

12 files changed

+554
-63
lines changed

src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap3HorizontalLayoutTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class FormExtensionBootstrap3HorizontalLayoutTest extends AbstractBootstrap3Hori
2929

3030
protected $testableFeatures = array(
3131
'choice_attr',
32+
'choice_label_attr',
3233
);
3334

3435
protected function setUp()

src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,11 +152,11 @@ public function createListFromLoader(ChoiceLoaderInterface $loader, $value = nul
152152
/**
153153
* {@inheritdoc}
154154
*/
155-
public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null)
155+
public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null, $labelAttr = null)
156156
{
157157
// The input is not validated on purpose. This way, the decorated
158158
// factory may decide which input to accept and which not.
159-
$hash = self::generateHash(array($list, $preferredChoices, $label, $index, $groupBy, $attr));
159+
$hash = self::generateHash(array($list, $preferredChoices, $label, $index, $groupBy, $attr, $labelAttr));
160160

161161
if (!isset($this->views[$hash])) {
162162
$this->views[$hash] = $this->decoratedFactory->createView(
@@ -165,7 +165,8 @@ public function createView(ChoiceListInterface $list, $preferredChoices = null,
165165
$label,
166166
$index,
167167
$groupBy,
168-
$attr
168+
$attr,
169+
$labelAttr
169170
);
170171
}
171172

src/Symfony/Component/Form/ChoiceList/Factory/ChoiceListFactoryInterface.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ public function createListFromLoader(ChoiceLoaderInterface $loader, $value = nul
6868
* a choice should not be grouped.
6969
* * The callable for the attributes should return an array of HTML
7070
* attributes that will be inserted in the tag of the choice.
71+
* * The callable for the label attributes should return an array of HTML
72+
* attributes that will be inserted in the tag of the label of the choice.
7173
*
7274
* If no callable is passed, the labels will be generated from the choice
7375
* keys. The view indices will be generated using an incrementing integer
@@ -90,8 +92,11 @@ public function createListFromLoader(ChoiceLoaderInterface $loader, $value = nul
9092
* group names
9193
* @param null|array|callable $attr The callable generating the
9294
* HTML attributes
95+
* @param null|array|callable $labelAttr The callable generating the
96+
* HTML attributes for choice
97+
* labels
9398
*
9499
* @return ChoiceListView The choice list view
95100
*/
96-
public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null);
101+
public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null, $labelAttr = null);
97102
}

src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public function createListFromLoader(ChoiceLoaderInterface $loader, $value = nul
4545
/**
4646
* {@inheritdoc}
4747
*/
48-
public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null)
48+
public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null, $labelAttr = null)
4949
{
5050
$preferredViews = array();
5151
$otherViews = array();
@@ -76,6 +76,7 @@ public function createView(ChoiceListInterface $list, $preferredChoices = null,
7676
$keys,
7777
$index,
7878
$attr,
79+
$labelAttr,
7980
$preferredChoices,
8081
$preferredViews,
8182
$otherViews
@@ -90,6 +91,7 @@ public function createView(ChoiceListInterface $list, $preferredChoices = null,
9091
$keys,
9192
$index,
9293
$attr,
94+
$labelAttr,
9395
$preferredChoices,
9496
$preferredViews,
9597
$otherViews
@@ -113,7 +115,7 @@ public function createView(ChoiceListInterface $list, $preferredChoices = null,
113115
return new ChoiceListView($otherViews, $preferredViews);
114116
}
115117

116-
private static function addChoiceView($choice, $value, $label, $keys, &$index, $attr, $isPreferred, &$preferredViews, &$otherViews)
118+
private static function addChoiceView($choice, $value, $label, $keys, &$index, $attr, $labelAttr, $isPreferred, &$preferredViews, &$otherViews)
117119
{
118120
// $value may be an integer or a string, since it's stored in the array
119121
// keys. We want to guarantee it's a string though.
@@ -137,7 +139,8 @@ private static function addChoiceView($choice, $value, $label, $keys, &$index, $
137139
$label,
138140
// The attributes may be a callable or a mapping from choice indices
139141
// to nested arrays
140-
is_callable($attr) ? call_user_func($attr, $choice, $key, $value) : (isset($attr[$key]) ? $attr[$key] : array())
142+
is_callable($attr) ? call_user_func($attr, $choice, $key, $value) : (isset($attr[$key]) ? $attr[$key] : array()),
143+
is_callable($labelAttr) ? call_user_func($labelAttr, $choice, $key, $value) : (isset($labelAttr[$key]) ? $labelAttr[$key] : array())
141144
);
142145

143146
// $isPreferred may be null if no choices are preferred
@@ -148,7 +151,7 @@ private static function addChoiceView($choice, $value, $label, $keys, &$index, $
148151
}
149152
}
150153

151-
private static function addChoiceViewsGroupedBy($groupBy, $label, $choices, $keys, &$index, $attr, $isPreferred, &$preferredViews, &$otherViews)
154+
private static function addChoiceViewsGroupedBy($groupBy, $label, $choices, $keys, &$index, $attr, $labelAttr, $isPreferred, &$preferredViews, &$otherViews)
152155
{
153156
foreach ($groupBy as $key => $value) {
154157
if (null === $value) {
@@ -167,6 +170,7 @@ private static function addChoiceViewsGroupedBy($groupBy, $label, $choices, $key
167170
$keys,
168171
$index,
169172
$attr,
173+
$labelAttr,
170174
$isPreferred,
171175
$preferredViewsForGroup,
172176
$otherViewsForGroup
@@ -191,14 +195,15 @@ private static function addChoiceViewsGroupedBy($groupBy, $label, $choices, $key
191195
$keys,
192196
$index,
193197
$attr,
198+
$labelAttr,
194199
$isPreferred,
195200
$preferredViews,
196201
$otherViews
197202
);
198203
}
199204
}
200205

201-
private static function addChoiceViewGroupedBy($groupBy, $choice, $value, $label, $keys, &$index, $attr, $isPreferred, &$preferredViews, &$otherViews)
206+
private static function addChoiceViewGroupedBy($groupBy, $choice, $value, $label, $keys, &$index, $attr, $labelAttr, $isPreferred, &$preferredViews, &$otherViews)
202207
{
203208
$groupLabel = call_user_func($groupBy, $choice, $keys[$value], $value);
204209

@@ -211,6 +216,7 @@ private static function addChoiceViewGroupedBy($groupBy, $choice, $value, $label
211216
$keys,
212217
$index,
213218
$attr,
219+
$labelAttr,
214220
$isPreferred,
215221
$preferredViews,
216222
$otherViews
@@ -235,6 +241,7 @@ private static function addChoiceViewGroupedBy($groupBy, $choice, $value, $label
235241
$keys,
236242
$index,
237243
$attr,
244+
$labelAttr,
238245
$isPreferred,
239246
$preferredViews[$groupLabel]->choices,
240247
$otherViews[$groupLabel]->choices

src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,11 @@ public function createListFromLoader(ChoiceLoaderInterface $loader, $value = nul
144144
* @param null|callable|string|PropertyPath $index The callable or path generating the view indices
145145
* @param null|callable|string|PropertyPath $groupBy The callable or path generating the group names
146146
* @param null|array|callable|string|PropertyPath $attr The callable or path generating the HTML attributes
147+
* @param null|array|callable|string|PropertyPath $labelAttr The callable or path generating the HTML label attributes
147148
*
148149
* @return ChoiceListView The choice list view
149150
*/
150-
public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null)
151+
public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null, $labelAttr = null)
151152
{
152153
$accessor = $this->propertyAccessor;
153154

@@ -210,6 +211,16 @@ public function createView(ChoiceListInterface $list, $preferredChoices = null,
210211
};
211212
}
212213

213-
return $this->decoratedFactory->createView($list, $preferredChoices, $label, $index, $groupBy, $attr);
214+
if (is_string($labelAttr) && !is_callable($labelAttr)) {
215+
$labelAttr = new PropertyPath($labelAttr);
216+
}
217+
218+
if ($labelAttr instanceof PropertyPath) {
219+
$labelAttr = function ($choice) use ($accessor, $labelAttr) {
220+
return $accessor->getValue($choice, $labelAttr);
221+
};
222+
}
223+
224+
return $this->decoratedFactory->createView($list, $preferredChoices, $label, $index, $groupBy, $attr, $labelAttr);
214225
}
215226
}

src/Symfony/Component/Form/ChoiceList/View/ChoiceView.php

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,28 @@ class ChoiceView
4646
*/
4747
public $attr;
4848

49+
/**
50+
* Additional attributes for the HTML label tag.
51+
*
52+
* @var array
53+
*/
54+
public $labelAttr;
55+
4956
/**
5057
* Creates a new choice view.
5158
*
52-
* @param mixed $data The original choice
53-
* @param string $value The view representation of the choice
54-
* @param string $label The label displayed to humans
55-
* @param array $attr Additional attributes for the HTML tag
59+
* @param mixed $data The original choice
60+
* @param string $value The view representation of the choice
61+
* @param string $label The label displayed to humans
62+
* @param array $attr Additional attributes for the HTML tag
63+
* @param array $labelAttr Additional attributes for the HTML label tag
5664
*/
57-
public function __construct($data, $value, $label, array $attr = array())
65+
public function __construct($data, $value, $label, array $attr = array(), array $labelAttr = array())
5866
{
5967
$this->data = $data;
6068
$this->value = $value;
6169
$this->label = $label;
6270
$this->attr = $attr;
71+
$this->labelAttr = $labelAttr;
6372
}
6473
}

src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ public function configureOptions(OptionsResolver $resolver)
320320
'choice_name' => null,
321321
'choice_value' => null,
322322
'choice_attr' => null,
323+
'choice_label_attr' => null,
323324
'preferred_choices' => array(),
324325
'group_by' => null,
325326
'empty_data' => $emptyData,
@@ -344,6 +345,7 @@ public function configureOptions(OptionsResolver $resolver)
344345
$resolver->setAllowedTypes('choice_name', array('null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
345346
$resolver->setAllowedTypes('choice_value', array('null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
346347
$resolver->setAllowedTypes('choice_attr', array('null', 'array', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
348+
$resolver->setAllowedTypes('choice_label_attr', array('null', 'array', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
347349
$resolver->setAllowedTypes('preferred_choices', array('array', '\Traversable', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
348350
$resolver->setAllowedTypes('group_by', array('null', 'array', '\Traversable', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
349351
}
@@ -410,6 +412,7 @@ private function addSubForm(FormBuilderInterface $builder, $name, ChoiceView $ch
410412
'value' => $choiceView->value,
411413
'label' => $choiceView->label,
412414
'attr' => $choiceView->attr,
415+
'label_attr' => $choiceView->labelAttr,
413416
'translation_domain' => $options['translation_domain'],
414417
'block_name' => 'entry',
415418
);
@@ -457,7 +460,8 @@ private function createChoiceListView(ChoiceListInterface $choiceList, array $op
457460
$options['choice_label'],
458461
$options['choice_name'],
459462
$options['group_by'],
460-
$options['choice_attr']
463+
$options['choice_attr'],
464+
$options['choice_label_attr']
461465
);
462466
}
463467
}

src/Symfony/Component/Form/Tests/AbstractBootstrap3LayoutTest.php

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,43 @@ public function testSingleChoiceExpandedAttributes()
885885
);
886886
}
887887

888+
public function testSingleChoiceExpandedLabelAttributes()
889+
{
890+
$form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array(
891+
'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'),
892+
'choice_label_attr' => array('Choice&B' => array('class' => 'foo&bar')),
893+
'multiple' => false,
894+
'expanded' => true,
895+
));
896+
897+
$this->assertWidgetMatchesXpath($form->createView(), array(),
898+
'/div
899+
[
900+
./div
901+
[@class="radio"]
902+
[
903+
./label
904+
[.=" [trans]Choice&A[/trans]"]
905+
[
906+
./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked]
907+
]
908+
]
909+
/following-sibling::div
910+
[@class="radio"]
911+
[
912+
./label
913+
[@class="foo&bar required"]
914+
[.=" [trans]Choice&B[/trans]"]
915+
[
916+
./input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)]
917+
]
918+
]
919+
/following-sibling::input[@type="hidden"][@id="name__token"][@class="form-control"]
920+
]
921+
'
922+
);
923+
}
924+
888925
public function testSingleChoiceExpandedWithPlaceholder()
889926
{
890927
$form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array(
@@ -1270,6 +1307,52 @@ public function testMultipleChoiceExpandedAttributes()
12701307
);
12711308
}
12721309

1310+
public function testMultipleChoiceExpandedLabelAttributes()
1311+
{
1312+
$form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', array('&a', '&c'), array(
1313+
'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b', 'Choice&C' => '&c'),
1314+
'choice_label_attr' => array('Choice&B' => array('class' => 'foo&bar')),
1315+
'multiple' => true,
1316+
'expanded' => true,
1317+
'required' => true,
1318+
));
1319+
1320+
$this->assertWidgetMatchesXpath($form->createView(), array(),
1321+
'/div
1322+
[
1323+
./div
1324+
[@class="checkbox"]
1325+
[
1326+
./label
1327+
[.=" [trans]Choice&A[/trans]"]
1328+
[
1329+
./input[@type="checkbox"][@name="name[]"][@id="name_0"][@checked][not(@required)]
1330+
]
1331+
]
1332+
/following-sibling::div
1333+
[@class="checkbox"]
1334+
[
1335+
./label[@class="foo&bar"]
1336+
[.=" [trans]Choice&B[/trans]"]
1337+
[
1338+
./input[@type="checkbox"][@name="name[]"][@id="name_1"][not(@checked)][not(@required)]
1339+
]
1340+
]
1341+
/following-sibling::div
1342+
[@class="checkbox"]
1343+
[
1344+
./label
1345+
[.=" [trans]Choice&C[/trans]"]
1346+
[
1347+
./input[@type="checkbox"][@name="name[]"][@id="name_2"][@checked][not(@required)]
1348+
]
1349+
]
1350+
/following-sibling::input[@type="hidden"][@id="name__token"][@class="form-control"]
1351+
]
1352+
'
1353+
);
1354+
}
1355+
12731356
public function testCountry()
12741357
{
12751358
$form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\CountryType', 'AT');

0 commit comments

Comments
 (0)