Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ public function getConfigTreeBuilder()
->booleanNode('polycollection')
->defaultTrue()
->end()
->booleanNode('choice_tree')
->defaultTrue()
->end()
->booleanNode('twig')
->defaultTrue()
->end()
Expand Down
4 changes: 4 additions & 0 deletions DependencyInjection/InfiniteFormExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand Down
47 changes: 47 additions & 0 deletions Form/Type/CheckboxLevelType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace Infinite\FormBundle\Form\Type;

use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class CheckboxLevelType extends CheckboxType
{
/**
* {@inheritdoc}
*/
public function getParent()
{
return 'checkbox';
}

/**
* {@inheritdoc}
*/
public function getName()
{
return 'infinite_form_checkbox_level';
}

/**
* {@inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
parent::setDefaultOptions($resolver);
$resolver->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'],
]);
}
}
148 changes: 148 additions & 0 deletions Form/Type/ChoiceTreeType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<?php

namespace Infinite\FormBundle\Form\Type;

use Symfony\Component\Form\Exception\LogicException;
use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToBooleanArrayTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToValuesTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToBooleanArrayTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer;
use Symfony\Component\Form\Extension\Core\EventListener\FixCheckboxInputListener;
use Symfony\Component\Form\Extension\Core\EventListener\FixRadioInputListener;
use Symfony\Component\Form\Extension\Core\EventListener\MergeCollectionListener;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

/**
* Class ChoiceTreeType based on ChoiceType.
*/
class ChoiceTreeType extends ChoiceType
{
/**
* Caches created choice lists.
*
* @var array
*/
private $treeChoiceListCache = [];

/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
// for symfony 2.7
$choiceList = method_exists($options['choice_list'], "getAdaptedList") ? $options['choice_list']->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);
}
}
}
}
47 changes: 47 additions & 0 deletions Form/Type/RadioLevelType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace Infinite\FormBundle\Form\Type;

use Symfony\Component\Form\Extension\Core\Type\RadioType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class RadioLevelType extends RadioType
{
/**
* {@inheritdoc}
*/
public function getParent()
{
return 'radio';
}

/**
* {@inheritdoc}
*/
public function getName()
{
return 'infinite_form_radio_level';
}

/**
* {@inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
parent::setDefaultOptions($resolver);
$resolver->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'],
]);
}
}
89 changes: 89 additions & 0 deletions Form/Type/TreeChoiceList.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

namespace Infinite\FormBundle\Form\Type;

use Symfony\Component\Form\Exception\InvalidConfigurationException;
use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList;
use Symfony\Component\Form\FormConfigBuilder;

class TreeChoiceList extends SimpleChoiceList
{
protected function addChoices(array &$bucketForPreferred, array &$bucketForRemaining, $choices, array $labels, array $preferredChoices, $level = 0)
{
// Add choices to the nested buckets
if (array_key_exists('label', $choices)) {
$this->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;
}
}
}
16 changes: 16 additions & 0 deletions Form/Type/TreeChoiceView.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Infinite\FormBundle\Form\Type;

use Symfony\Component\Form\Extension\Core\View\ChoiceView;

class TreeChoiceView extends ChoiceView
{
public $level;

public function __construct($data, $value, $label, $level)
{
parent::__construct($data, $value, $label);
$this->level = $level;
}
}
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading