Skip to content

Commit 9c38a1b

Browse files
committed
Merge pull request #1 from Charlie-Lucas/feature/add-choice-tree-form-type
Feature/add choice tree form type
2 parents cece91a + ddc9574 commit 9c38a1b

File tree

11 files changed

+549
-0
lines changed

11 files changed

+549
-0
lines changed

DependencyInjection/Configuration.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ public function getConfigTreeBuilder()
4444
->booleanNode('polycollection')
4545
->defaultTrue()
4646
->end()
47+
->booleanNode('choice_tree')
48+
->defaultTrue()
49+
->end()
4750
->booleanNode('twig')
4851
->defaultTrue()
4952
->end()

DependencyInjection/InfiniteFormExtension.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ public function load(array $configs, ContainerBuilder $container)
5454
$loader->load('polycollection.xml');
5555
}
5656

57+
if ($configs['choice_tree']) {
58+
$loader->load('choice_tree.xml');
59+
}
60+
5761
if ($configs['twig']) {
5862
$loader->load('twig.xml');
5963
}

Form/Type/CheckboxLevelType.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
namespace Infinite\FormBundle\Form\Type;
4+
5+
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
6+
use Symfony\Component\Form\FormInterface;
7+
use Symfony\Component\Form\FormView;
8+
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
9+
10+
class CheckboxLevelType extends CheckboxType
11+
{
12+
/**
13+
* {@inheritdoc}
14+
*/
15+
public function getParent()
16+
{
17+
return 'checkbox';
18+
}
19+
20+
/**
21+
* {@inheritdoc}
22+
*/
23+
public function getName()
24+
{
25+
return 'infinite_form_checkbox_level';
26+
}
27+
28+
/**
29+
* {@inheritdoc}
30+
*/
31+
public function setDefaultOptions(OptionsResolverInterface $resolver)
32+
{
33+
parent::setDefaultOptions($resolver);
34+
$resolver->setDefaults(['level' => 0]);
35+
}
36+
37+
/**
38+
* {@inheritdoc}
39+
*/
40+
public function buildView(FormView $view, FormInterface $form, array $options)
41+
{
42+
parent::buildView($view, $form, $options);
43+
$view->vars = array_replace($view->vars, [
44+
'level' => $options['level'],
45+
]);
46+
}
47+
}

Form/Type/ChoiceTreeType.php

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<?php
2+
3+
namespace Infinite\FormBundle\Form\Type;
4+
5+
use Symfony\Component\Form\Exception\LogicException;
6+
use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToBooleanArrayTransformer;
7+
use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToValuesTransformer;
8+
use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToBooleanArrayTransformer;
9+
use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer;
10+
use Symfony\Component\Form\Extension\Core\EventListener\FixCheckboxInputListener;
11+
use Symfony\Component\Form\Extension\Core\EventListener\FixRadioInputListener;
12+
use Symfony\Component\Form\Extension\Core\EventListener\MergeCollectionListener;
13+
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
14+
use Symfony\Component\Form\FormBuilderInterface;
15+
use Symfony\Component\OptionsResolver\Options;
16+
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
17+
18+
/**
19+
* Class ChoiceTreeType based on ChoiceType.
20+
*/
21+
class ChoiceTreeType extends ChoiceType
22+
{
23+
/**
24+
* Caches created choice lists.
25+
*
26+
* @var array
27+
*/
28+
private $treeChoiceListCache = [];
29+
30+
/**
31+
* {@inheritdoc}
32+
*/
33+
public function buildForm(FormBuilderInterface $builder, array $options)
34+
{
35+
$choiceList = $options['choice_list']->getAdaptedList();
36+
if (!$choiceList && !is_array($options['choices']) && !$options['choices'] instanceof \Traversable) {
37+
throw new LogicException('Either the option "choices" or "choice_list" must be set.');
38+
}
39+
40+
if ($options['expanded']) {
41+
$preferredViews = $choiceList->getPreferredViews();
42+
$remainingViews = $choiceList->getRemainingViews();
43+
44+
// Check if the choices already contain the empty value
45+
// Only add the empty value option if this is not the case
46+
if (null !== $options['placeholder'] && 0 === count($choiceList->getChoicesForValues(['']))) {
47+
$placeholderView = new TreeChoiceView(null, '', $options['placeholder'], 0);
48+
49+
// "placeholder" is a reserved index
50+
$this->addSubForms($builder, ['placeholder' => $placeholderView], $options);
51+
}
52+
53+
$this->addSubForms($builder, $preferredViews, $options);
54+
$this->addSubForms($builder, $remainingViews, $options);
55+
56+
if ($options['multiple']) {
57+
$builder->addViewTransformer(new ChoicesToBooleanArrayTransformer($options['choice_list']));
58+
$builder->addEventSubscriber(new FixCheckboxInputListener($options['choice_list']), 10);
59+
} else {
60+
$builder->addViewTransformer(new ChoiceToBooleanArrayTransformer($options['choice_list'], $builder->has('placeholder')));
61+
$builder->addEventSubscriber(new FixRadioInputListener($options['choice_list'], $builder->has('placeholder')), 10);
62+
}
63+
} else {
64+
if ($options['multiple']) {
65+
$builder->addViewTransformer(new ChoicesToValuesTransformer($options['choice_list']));
66+
} else {
67+
$builder->addViewTransformer(new ChoiceToValueTransformer($options['choice_list']));
68+
}
69+
}
70+
71+
if ($options['multiple'] && $options['by_reference']) {
72+
// Make sure the collection created during the client->norm
73+
// transformation is merged back into the original collection
74+
$builder->addEventSubscriber(new MergeCollectionListener(true, true));
75+
}
76+
}
77+
78+
/**
79+
* {@inheritdoc}
80+
*/
81+
public function setDefaultOptions(OptionsResolverInterface $resolver)
82+
{
83+
parent::setDefaultOptions($resolver);
84+
85+
//set our custom choice list
86+
$treeChoiceListCache = &$this->treeChoiceListCache;
87+
88+
$treeChoiceList = function (Options $options) use (&$treeChoiceListCache) {
89+
// Harden against NULL values (like in EntityType and ModelType)
90+
$choices = null !== $options['choices'] ? $options['choices'] : [];
91+
92+
// Reuse existing choice lists in order to increase performance
93+
$hash = hash('sha256', serialize([$choices, $options['preferred_choices']]));
94+
95+
if (!isset($treeChoiceListCache[$hash])) {
96+
$treeChoiceListCache[$hash] = new TreeChoiceList($choices, $options['preferred_choices']);
97+
}
98+
99+
return $treeChoiceListCache[$hash];
100+
};
101+
102+
$resolver->setDefaults(['choice_list' => $treeChoiceList]);
103+
}
104+
105+
/**
106+
* {@inheritdoc}
107+
*/
108+
public function getName()
109+
{
110+
return 'infinite_form_choice_tree';
111+
}
112+
113+
/**
114+
* Adds the sub fields for an expanded choice field.
115+
*
116+
* @param FormBuilderInterface $builder The form builder.
117+
* @param array $choiceViews The choice view objects.
118+
* @param array $options The build options.
119+
*/
120+
private function addSubForms(FormBuilderInterface $builder, array $choiceViews, array $options)
121+
{
122+
foreach ($choiceViews as $i => $choiceView) {
123+
if (is_array($choiceView)) {
124+
// Flatten groups
125+
$this->addSubForms($builder, $choiceView, $options);
126+
} else {
127+
$choiceOpts = [
128+
'value' => $choiceView->value,
129+
'label' => $choiceView->label,
130+
'level' => $choiceView->level,
131+
'translation_domain' => $options['translation_domain'],
132+
'block_name' => 'entry',
133+
];
134+
135+
if ($options['multiple']) {
136+
$choiceType = 'infinite_form_checkbox_level';
137+
// The user can check 0 or more checkboxes. If required
138+
// is true, he is required to check all of them.
139+
$choiceOpts['required'] = false;
140+
} else {
141+
$choiceType = 'infinite_form_radio_level';
142+
}
143+
$builder->add($i, $choiceType, $choiceOpts);
144+
}
145+
}
146+
}
147+
}

Form/Type/RadioLevelType.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
namespace Infinite\FormBundle\Form\Type;
4+
5+
use Symfony\Component\Form\Extension\Core\Type\RadioType;
6+
use Symfony\Component\Form\FormInterface;
7+
use Symfony\Component\Form\FormView;
8+
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
9+
10+
class RadioLevelType extends RadioType
11+
{
12+
/**
13+
* {@inheritdoc}
14+
*/
15+
public function getParent()
16+
{
17+
return 'radio';
18+
}
19+
20+
/**
21+
* {@inheritdoc}
22+
*/
23+
public function getName()
24+
{
25+
return 'infinite_form_radio_level';
26+
}
27+
28+
/**
29+
* {@inheritdoc}
30+
*/
31+
public function setDefaultOptions(OptionsResolverInterface $resolver)
32+
{
33+
parent::setDefaultOptions($resolver);
34+
$resolver->setDefaults(['level' => 0]);
35+
}
36+
37+
/**
38+
* {@inheritdoc}
39+
*/
40+
public function buildView(FormView $view, FormInterface $form, array $options)
41+
{
42+
parent::buildView($view, $form, $options);
43+
$view->vars = array_replace($view->vars, [
44+
'level' => $options['level'],
45+
]);
46+
}
47+
}

Form/Type/TreeChoiceList.php

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
namespace Infinite\FormBundle\Form\Type;
4+
5+
use Symfony\Component\Form\Exception\InvalidConfigurationException;
6+
use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList;
7+
use Symfony\Component\Form\FormConfigBuilder;
8+
9+
class TreeChoiceList extends SimpleChoiceList
10+
{
11+
protected function addChoices(array &$bucketForPreferred, array &$bucketForRemaining, $choices, array $labels, array $preferredChoices, $level = 0)
12+
{
13+
// Add choices to the nested buckets
14+
if (array_key_exists('label', $choices)) {
15+
$this->addChoice(
16+
$bucketForPreferred,
17+
$bucketForRemaining,
18+
$choices['value'],
19+
$choices['label'],
20+
$preferredChoices,
21+
$level
22+
);
23+
24+
if (count($choices['choice_list']) > 0) {
25+
$this->addChoices($bucketForPreferred, $bucketForRemaining, $choices['choice_list'], $labels, $preferredChoices, $level + 1);
26+
}
27+
} else {
28+
foreach ($choices as $choice => $label) {
29+
if (is_array($label)) {
30+
$this->addChoices(
31+
$bucketForPreferred,
32+
$bucketForRemaining,
33+
$label,
34+
$labels,
35+
$preferredChoices,
36+
$level
37+
);
38+
} else {
39+
$this->addChoice(
40+
$bucketForPreferred,
41+
$bucketForRemaining,
42+
$choice,
43+
$label,
44+
$preferredChoices,
45+
$level
46+
);
47+
}
48+
}
49+
}
50+
}
51+
52+
/**
53+
* Adds a new choice.
54+
*
55+
* @param array $bucketForPreferred The bucket where to store the preferred
56+
* view objects.
57+
* @param array $bucketForRemaining The bucket where to store the
58+
* non-preferred view objects.
59+
* @param mixed $choice The choice to add.
60+
* @param string $label The label for the choice.
61+
* @param array $preferredChoices The preferred choices.
62+
*
63+
* @throws InvalidConfigurationException If no valid value or index could be created.
64+
*/
65+
protected function addChoice(array &$bucketForPreferred, array &$bucketForRemaining, $choice, $label, array $preferredChoices, $level = 0)
66+
{
67+
$index = $this->createIndex($choice);
68+
69+
if ('' === $index || null === $index || !FormConfigBuilder::isValidName((string) $index)) {
70+
throw new InvalidConfigurationException(sprintf('The index "%s" created by the choice list is invalid. It should be a valid, non-empty Form name.', $index));
71+
}
72+
73+
$value = $this->createValue($choice);
74+
75+
if (!is_string($value)) {
76+
throw new InvalidConfigurationException(sprintf('The value created by the choice list is of type "%s", but should be a string.', gettype($value)));
77+
}
78+
$view = new TreeChoiceView($choice, $value, $label, $level);
79+
80+
$this->choices[$index] = $this->fixChoice($choice);
81+
$this->values[$index] = $value;
82+
83+
if ($this->isPreferred($choice, $preferredChoices)) {
84+
$bucketForPreferred[$index] = $view;
85+
} else {
86+
$bucketForRemaining[$index] = $view;
87+
}
88+
}
89+
}

Form/Type/TreeChoiceView.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace Infinite\FormBundle\Form\Type;
4+
5+
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
6+
7+
class TreeChoiceView extends ChoiceView
8+
{
9+
public $level;
10+
11+
public function __construct($data, $value, $label, $level)
12+
{
13+
parent::__construct($data, $value, $label);
14+
$this->level = $level;
15+
}
16+
}

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,16 @@ for use when rendering templates.
6060

6161
For more information see the [Twig Helper][].
6262

63+
Choice Tree
64+
--------------
65+
66+
The choice Tree form Type allows have a tree for choices.
67+
68+
For more information see the [Choice Tree Documentation][].
69+
6370
[PolyCollection Documentation]: https://github.com/infinite-networks/InfiniteFormBundle/blob/master/Resources/doc/polycollection.md
6471
[Collection Helper Documentation]: https://github.com/infinite-networks/InfiniteFormBundle/blob/master/Resources/doc/collection-helper.md
6572
[CheckboxGrid Documentation]: https://github.com/infinite-networks/InfiniteFormBundle/blob/master/Resources/doc/checkboxgrid.md
6673
[Twig Helper]: https://github.com/infinite-networks/InfiniteFormBundle/blob/master/Resources/doc/twig-helper.md
74+
[Choice Tree Documentation]: https://github.com/Charlie-Lucas/InfiniteFormBundle/blob/feature/add-choice-tree-form-type/Resources/doc/choice_tree.md
6775
[can be found here]: https://github.com/infinite-networks/InfiniteFormBundle/blob/master/Resources/doc/installation.md

0 commit comments

Comments
 (0)