diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php
index c15edee..3d6ffb5 100644
--- a/DependencyInjection/Configuration.php
+++ b/DependencyInjection/Configuration.php
@@ -44,6 +44,9 @@ public function getConfigTreeBuilder()
->booleanNode('polycollection')
->defaultTrue()
->end()
+ ->booleanNode('choice_tree')
+ ->defaultTrue()
+ ->end()
->booleanNode('twig')
->defaultTrue()
->end()
diff --git a/DependencyInjection/InfiniteFormExtension.php b/DependencyInjection/InfiniteFormExtension.php
index 5221e97..475eec5 100644
--- a/DependencyInjection/InfiniteFormExtension.php
+++ b/DependencyInjection/InfiniteFormExtension.php
@@ -54,6 +54,10 @@ public function load(array $configs, ContainerBuilder $container)
$loader->load('polycollection.xml');
}
+ if ($configs['choice_tree']) {
+ $loader->load('choice_tree.xml');
+ }
+
if ($configs['twig']) {
$loader->load('twig.xml');
}
diff --git a/Form/Type/CheckboxLevelType.php b/Form/Type/CheckboxLevelType.php
new file mode 100644
index 0000000..ad7c073
--- /dev/null
+++ b/Form/Type/CheckboxLevelType.php
@@ -0,0 +1,47 @@
+setDefaults(['level' => 0]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildView(FormView $view, FormInterface $form, array $options)
+ {
+ parent::buildView($view, $form, $options);
+ $view->vars = array_replace($view->vars, [
+ 'level' => $options['level'],
+ ]);
+ }
+}
diff --git a/Form/Type/ChoiceTreeType.php b/Form/Type/ChoiceTreeType.php
new file mode 100644
index 0000000..6a035dd
--- /dev/null
+++ b/Form/Type/ChoiceTreeType.php
@@ -0,0 +1,148 @@
+getAdaptedList(): $options['choice_list'];
+ if (!$choiceList && !is_array($options['choices']) && !$options['choices'] instanceof \Traversable) {
+ throw new LogicException('Either the option "choices" or "choice_list" must be set.');
+ }
+
+ if ($options['expanded']) {
+ $preferredViews = $choiceList->getPreferredViews();
+ $remainingViews = $choiceList->getRemainingViews();
+
+ // Check if the choices already contain the empty value
+ // Only add the empty value option if this is not the case
+ if (null !== $options['placeholder'] && 0 === count($choiceList->getChoicesForValues(['']))) {
+ $placeholderView = new TreeChoiceView(null, '', $options['placeholder'], 0);
+
+ // "placeholder" is a reserved index
+ $this->addSubForms($builder, ['placeholder' => $placeholderView], $options);
+ }
+
+ $this->addSubForms($builder, $preferredViews, $options);
+ $this->addSubForms($builder, $remainingViews, $options);
+
+ if ($options['multiple']) {
+ $builder->addViewTransformer(new ChoicesToBooleanArrayTransformer($options['choice_list']));
+ $builder->addEventSubscriber(new FixCheckboxInputListener($options['choice_list']), 10);
+ } else {
+ $builder->addViewTransformer(new ChoiceToBooleanArrayTransformer($options['choice_list'], $builder->has('placeholder')));
+ $builder->addEventSubscriber(new FixRadioInputListener($options['choice_list'], $builder->has('placeholder')), 10);
+ }
+ } else {
+ if ($options['multiple']) {
+ $builder->addViewTransformer(new ChoicesToValuesTransformer($options['choice_list']));
+ } else {
+ $builder->addViewTransformer(new ChoiceToValueTransformer($options['choice_list']));
+ }
+ }
+
+ if ($options['multiple'] && $options['by_reference']) {
+ // Make sure the collection created during the client->norm
+ // transformation is merged back into the original collection
+ $builder->addEventSubscriber(new MergeCollectionListener(true, true));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ parent::setDefaultOptions($resolver);
+
+ //set our custom choice list
+ $treeChoiceListCache = &$this->treeChoiceListCache;
+
+ $treeChoiceList = function (Options $options) use (&$treeChoiceListCache) {
+ // Harden against NULL values (like in EntityType and ModelType)
+ $choices = null !== $options['choices'] ? $options['choices'] : [];
+
+ // Reuse existing choice lists in order to increase performance
+ $hash = hash('sha256', serialize([$choices, $options['preferred_choices']]));
+
+ if (!isset($treeChoiceListCache[$hash])) {
+ $treeChoiceListCache[$hash] = new TreeChoiceList($choices, $options['preferred_choices']);
+ }
+
+ return $treeChoiceListCache[$hash];
+ };
+
+ $resolver->setDefaults(['choice_list' => $treeChoiceList]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'infinite_form_choice_tree';
+ }
+
+ /**
+ * Adds the sub fields for an expanded choice field.
+ *
+ * @param FormBuilderInterface $builder The form builder.
+ * @param array $choiceViews The choice view objects.
+ * @param array $options The build options.
+ */
+ private function addSubForms(FormBuilderInterface $builder, array $choiceViews, array $options)
+ {
+ foreach ($choiceViews as $i => $choiceView) {
+ if (is_array($choiceView)) {
+ // Flatten groups
+ $this->addSubForms($builder, $choiceView, $options);
+ } else {
+ $choiceOpts = [
+ 'value' => $choiceView->value,
+ 'label' => $choiceView->label,
+ 'level' => $choiceView->level,
+ 'translation_domain' => $options['translation_domain'],
+ 'block_name' => 'entry',
+ ];
+
+ if ($options['multiple']) {
+ $choiceType = 'infinite_form_checkbox_level';
+ // The user can check 0 or more checkboxes. If required
+ // is true, he is required to check all of them.
+ $choiceOpts['required'] = false;
+ } else {
+ $choiceType = 'infinite_form_radio_level';
+ }
+ $builder->add($i, $choiceType, $choiceOpts);
+ }
+ }
+ }
+}
diff --git a/Form/Type/RadioLevelType.php b/Form/Type/RadioLevelType.php
new file mode 100644
index 0000000..7924d31
--- /dev/null
+++ b/Form/Type/RadioLevelType.php
@@ -0,0 +1,47 @@
+setDefaults(['level' => 0]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildView(FormView $view, FormInterface $form, array $options)
+ {
+ parent::buildView($view, $form, $options);
+ $view->vars = array_replace($view->vars, [
+ 'level' => $options['level'],
+ ]);
+ }
+}
diff --git a/Form/Type/TreeChoiceList.php b/Form/Type/TreeChoiceList.php
new file mode 100644
index 0000000..d88232c
--- /dev/null
+++ b/Form/Type/TreeChoiceList.php
@@ -0,0 +1,89 @@
+addChoice(
+ $bucketForPreferred,
+ $bucketForRemaining,
+ $choices['value'],
+ $choices['label'],
+ $preferredChoices,
+ $level
+ );
+
+ if (count($choices['choice_list']) > 0) {
+ $this->addChoices($bucketForPreferred, $bucketForRemaining, $choices['choice_list'], $labels, $preferredChoices, $level + 1);
+ }
+ } else {
+ foreach ($choices as $choice => $label) {
+ if (is_array($label)) {
+ $this->addChoices(
+ $bucketForPreferred,
+ $bucketForRemaining,
+ $label,
+ $labels,
+ $preferredChoices,
+ $level
+ );
+ } else {
+ $this->addChoice(
+ $bucketForPreferred,
+ $bucketForRemaining,
+ $choice,
+ $label,
+ $preferredChoices,
+ $level
+ );
+ }
+ }
+ }
+ }
+
+ /**
+ * Adds a new choice.
+ *
+ * @param array $bucketForPreferred The bucket where to store the preferred
+ * view objects.
+ * @param array $bucketForRemaining The bucket where to store the
+ * non-preferred view objects.
+ * @param mixed $choice The choice to add.
+ * @param string $label The label for the choice.
+ * @param array $preferredChoices The preferred choices.
+ *
+ * @throws InvalidConfigurationException If no valid value or index could be created.
+ */
+ protected function addChoice(array &$bucketForPreferred, array &$bucketForRemaining, $choice, $label, array $preferredChoices, $level = 0)
+ {
+ $index = $this->createIndex($choice);
+
+ if ('' === $index || null === $index || !FormConfigBuilder::isValidName((string) $index)) {
+ throw new InvalidConfigurationException(sprintf('The index "%s" created by the choice list is invalid. It should be a valid, non-empty Form name.', $index));
+ }
+
+ $value = $this->createValue($choice);
+
+ if (!is_string($value)) {
+ throw new InvalidConfigurationException(sprintf('The value created by the choice list is of type "%s", but should be a string.', gettype($value)));
+ }
+ $view = new TreeChoiceView($choice, $value, $label, $level);
+
+ $this->choices[$index] = $this->fixChoice($choice);
+ $this->values[$index] = $value;
+
+ if ($this->isPreferred($choice, $preferredChoices)) {
+ $bucketForPreferred[$index] = $view;
+ } else {
+ $bucketForRemaining[$index] = $view;
+ }
+ }
+}
diff --git a/Form/Type/TreeChoiceView.php b/Form/Type/TreeChoiceView.php
new file mode 100644
index 0000000..ed5c6fa
--- /dev/null
+++ b/Form/Type/TreeChoiceView.php
@@ -0,0 +1,16 @@
+level = $level;
+ }
+}
diff --git a/README.md b/README.md
index 7d81fb6..bdb1c0e 100644
--- a/README.md
+++ b/README.md
@@ -60,8 +60,16 @@ for use when rendering templates.
For more information see the [Twig Helper][].
+Choice Tree
+--------------
+
+The choice Tree form Type allows have a tree for choices.
+
+For more information see the [Choice Tree Documentation][].
+
[PolyCollection Documentation]: https://github.com/infinite-networks/InfiniteFormBundle/blob/master/Resources/doc/polycollection.md
[Collection Helper Documentation]: https://github.com/infinite-networks/InfiniteFormBundle/blob/master/Resources/doc/collection-helper.md
[CheckboxGrid Documentation]: https://github.com/infinite-networks/InfiniteFormBundle/blob/master/Resources/doc/checkboxgrid.md
[Twig Helper]: https://github.com/infinite-networks/InfiniteFormBundle/blob/master/Resources/doc/twig-helper.md
+[Choice Tree Documentation]: https://github.com/Charlie-Lucas/InfiniteFormBundle/blob/feature/add-choice-tree-form-type/Resources/doc/choice_tree.md
[can be found here]: https://github.com/infinite-networks/InfiniteFormBundle/blob/master/Resources/doc/installation.md
diff --git a/Resources/config/choice_tree.xml b/Resources/config/choice_tree.xml
new file mode 100644
index 0000000..59d252c
--- /dev/null
+++ b/Resources/config/choice_tree.xml
@@ -0,0 +1,25 @@
+
+