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 @@ + + + + + Infinite\FormBundle\Form\Type\ChoiceTreeType + Infinite\FormBundle\Form\Type\RadioLevelType + Infinite\FormBundle\Form\Type\CheckboxLevelType + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Resources/doc/choice_tree.md b/Resources/doc/choice_tree.md new file mode 100644 index 0000000..a3e851d --- /dev/null +++ b/Resources/doc/choice_tree.md @@ -0,0 +1,93 @@ +InfiniteFormBundle's Choice Tree Form Type +============================================= + +Introduction +------------ + +The choice tree form type allow you to display a tree of choice, a simple +choice list give you the possibility to generate group but just for one level. + +Installation +------------ + +[Installation of InfiniteFormBundle](installation.md) is covered in a separate +document. The Choice Tree type is automatically enabled when the bundle is +installed. + +Object Structure +---------------- + +The Choice Tree type work as a choice but choices must implement a specific pattern: +```php + $tree = { + 0 => [ + "value" => 1, + "label" => Object/string, + "choice_list" =>{ + 0 => [ + "value" => 3, + "label" => Object/string, + "choice_list" => [] + ] + } + ], + 1 => [ + "value" => 2, + "label" => Object/string, + "choice_list" =>{ + 0 => [ + "value" => 1, + "label" => Object/string, + "choice_list" => [] + ] + } + ] + } + $builder + ->add( + 'category', 'infinite_form_choice_tree', [ + 'choices' => $tree, + ] + ); + +``` +For example when you use the Nested Tree Repository from Gedmo : + +```php + class MyFormType extends AbstractType + { + public function buildForm(FormBuilderInterface $builder, array $options) + { + $tree = $myRepo->buildTree($myRepo->getNodesHierarchy()); + // then we need recursively parse this tree for the form with a recursive function + $builder + ->add( + 'category', 'infinite_form_choice_tree', [ + 'choices' => $this->rebuildTree($tree), + ] + ); + + } + + + public function rebuildTree($tree) + { + $hierarchy = []; + foreach ($tree as $children) { + $node = []; + $node['label'] = $category['title']; + $node['value'] = $category['id']; + $node['choice_list'] = $this->rebuildTree($children); + $hierarchy[] = $node; + + } + return $hierarchy; + } + } +``` + +#TODO +---------------- + +L'utilisation de paramètres dans le form type devraient permettre de parser ce qui est passer par "choices", +pour éviter ainsi d'avoir à implémenter une méthode de parsing \ No newline at end of file diff --git a/Resources/views/form_theme.html.twig b/Resources/views/form_theme.html.twig index 44e446b..c209ff5 100644 --- a/Resources/views/form_theme.html.twig +++ b/Resources/views/form_theme.html.twig @@ -50,3 +50,73 @@ file that was distributed with this source code. {% endif %} {% endblock infinite_form_attachment_widget %} +{%- block infinite_form_choice_tree_widget -%} + {% if expanded %} + {{- block('infinite_form_choice_tree_widget_expanded') -}} + {% else %} + {{- block('infinite_form_choice_tree_widget_collapsed') -}} + {% endif %} +{%- endblock infinite_form_choice_tree_widget -%} + +{%- block infinite_form_choice_tree_widget_expanded -%} +
+ {%- for child in form %} + {% set marginLeft = 0 %} + {% if child.vars.level > 0 %} + {% for i in 1..child.vars.level %} + {% set marginLeft = marginLeft + 20 %} + {% endfor %} + {% endif %} +
+ {{- form_widget(child) -}} + {{- form_label(child, null, {translation_domain: choice_translation_domain}) -}} +
+ {% endfor -%} +
+{%- endblock infinite_form_choice_tree_widget_expanded -%} + +{%- block infinite_form_choice_tree_widget_collapsed -%} + {%- if required and placeholder is none and not placeholder_in_choices and not multiple -%} + {% set required = false %} + {%- endif -%} + +{%- endblock infinite_form_choice_tree_widget_collapsed -%} + +{%- block infinite_form_choice_tree_widget_options -%} + {% for group_label, choice in options %} + {%- if choice is iterable -%} + {% set options = choice %} + {{- block('infinite_form_choice_tree_widget_options') -}} + {%- else -%} + {% set index = "" %} + {% if choice.level > 0 %} + {% for i in 1..choice.level %} + {% set index = index ~ "  " %} + {% endfor %} + {% endif %} + {% set attr = choice.attr %} + + {%- endif -%} + {% endfor %} +{%- endblock infinite_form_choice_tree_widget_options -%} + +{%- block infinite_form_checkbox_level_widget -%} + +{%- endblock infinite_form_checkbox_level_widget -%} + +{%- block infinite_form_radio_level_widget -%} + +{%- endblock infinite_form_radio_level_widget -%}