I need to create form element with following logic:
Here is HTML markup that represents this logic:
Choose your budget:
<div id="briefing_budget" class="budget clearfix">
<label>
<input type="radio" id="briefing_budget_0" name="briefing[budget][selected]" required="required" value="9999"> 9 999 rubles
</label>
<label>
<input type="radio" name="briefing[budget][selected]" value="other"> other <input type="text" name="briefing[budget][number]">
</label>
</div>
To make this work, I've created custom field type with custom Twig block. Finnaly, I've got something that I don't really like...
Here is the code of custom type:
<?php
namespace Company\Optimal\PromoAction\FreeCampaignFor10k;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Exception\LogicException;
use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList;
use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToBooleanArrayTransformer;
use Symfony\Component\Form\Extension\Core\EventListener\FixRadioInputListener;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
/**
* choices : [{"<value>": "<label>"}]
*/
class NumberRadioType extends AbstractType
{
/**
* Caches created choice lists.
* @var array
*/
private $choiceListCache = array();
public function buildForm(FormBuilderInterface $builder, array $options)
{
if (!$options['choice_list'] && !is_array($options['choices']) && !$options['choices'] instanceof \Traversable) {
throw new LogicException('Either the option "choices" or "choice_list" must be set.');
}
$preferredViews = $options['choice_list']->getPreferredViews();
$remainingViews = $options['choice_list']->getRemainingViews();
if (null !== $options['empty_value'] && 0 === count($options['choice_list']->getChoicesForValues(array('')))) {
$placeholderView = new ChoiceView(null, '', $options['empty_value']);
$this->addSubForms($builder, array('placeholder' => $placeholderView), $options);
}
$this->addSubForms($builder, $preferredViews, $options);
$this->addSubForms($builder, $remainingViews, $options);
$builder->addViewTransformer(new ChoiceToBooleanArrayTransformer($options['choice_list'], $builder->has('placeholder')));
$builder->addEventSubscriber(new FixRadioInputListener($options['choice_list'], $builder->has('placeholder')), 10);
$name = $builder->getName();
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($name) {
$data = $event->getData();
$data = $data['selected'] == 'other' ? $data['number'] : $data['selected'];
$event->setData($data);
});
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars = array_replace($view->vars, array(
'preferred_choices' => $options['choice_list']->getPreferredViews(),
'choices' => $options['choice_list']->getRemainingViews(),
'separator' => '-------------------',
'empty_value' => null,
));
$view->vars['is_selected'] = function ($choice, $value) {
return $choice === $value;
};
$view->vars['empty_value_in_choices'] = 0 !== count($options['choice_list']->getChoicesForValues(array('')));
if (null !== $options['empty_value'] && !$view->vars['empty_value_in_choices']) {
$view->vars['empty_value'] = $options['empty_value'];
}
}
public function finishView(FormView $view, FormInterface $form, array $options)
{
foreach ($view as $childView) {
$childView->vars['full_name'] = $view->vars['full_name'] . '[selected]';
}
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$choiceListCache =& $this->choiceListCache;
$choiceList = function (Options $options) use (&$choiceListCache) {
$choices = null !== $options['choices'] ? $options['choices'] : array();
// Reuse existing choice lists in order to increase performance
$hash = hash('sha256', json_encode(array($choices, $options['preferred_choices'])));
if (!isset($choiceListCache[$hash])) {
$choiceListCache[$hash] = new SimpleChoiceList($choices, $options['preferred_choices']);
}
return $choiceListCache[$hash];
};
$emptyData = array();
$emptyValue = function (Options $options) {
return $options['required'] ? null : '';
};
$emptyValueNormalizer = function (Options $options, $emptyValue) {
if (false === $emptyValue) {
return;
} elseif ('' === $emptyValue) {
return 'None';
}
return $emptyValue;
};
$resolver->setDefaults(array(
'choice_list' => $choiceList,
'choices' => array(),
'preferred_choices' => array(),
'empty_data' => $emptyData,
'empty_value' => $emptyValue,
'error_bubbling' => false,
'compound' => true,
'data_class' => null,
));
$resolver->setNormalizers(array(
'empty_value' => $emptyValueNormalizer,
));
$resolver->setAllowedTypes(array(
'choice_list' => array('null', 'Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface'),
));
}
/**
* Returns the name of this type.
*
* @return string The name of this type
*/
public function getName()
{
return 'number_radio';
}
/**
* 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 = array(
'value' => $choiceView->value,
'label' => $choiceView->label,
'translation_domain' => $options['translation_domain'],
);
$choiceType = 'radio';
$builder->add($i, $choiceType, $choiceOpts);
}
}
}
}
And template:
{% block number_radio_widget %}
{% spaceless %}
<div {{ block('widget_container_attributes') }}>
{% for child in form %}
<label>{{ form_widget(child) }}{{ child.vars.label }}</label>
{% endfor %}
<label><input type="radio" name="{{ form.vars.full_name }}[selected]" value="other"/>
other <input type="text" name="{{ form.vars.full_name }}[number]"/>
</label>
</div>
{% endspaceless %}
{% endblock %}
I'm newbie in Symfony, so I copypasted a lot from Symfony's class ChoiceType
, and actualy don't know purpose of half of stuff that happens there. :)
Finnaly, the question is: What is the best (or at least better) way to achieve what I have had achieved using Symfony 2 form component?
If you heritate the FormType from "text", it will be much simple to handle with some javascript added in twig
Set the minimum required in the form type :
class NumberRadioType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->setAttribute('configs', $options['configs']);
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['configs'] = $form->getConfig()->getAttribute('configs');
}
/**
* {@inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'configs' => array()
));
$resolver->setNormalizers(array(
'configs' => function (Options $options, $value) {
return true;
}
));
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return 'text';
}
/**
* {@inheritdoc}
*/
public function getName()
{
return 'NumberRadio';
}
in html twig template as you did :
{% block number_radio_widget %}
{% spaceless %}
<div {{ block('widget_container_attributes') }}>
{% for child in form %}
<label>{{ form_widget(child) }}{{ child.vars.label }}</label>
{% endfor %}
<label><input type="radio" {{id}}/>
other <input type="text" name="{{ form.vars.full_name }}[number]" value="{{value}}"/>
</label>
</div>
{% endspaceless %}
{% endblock %}
you can use {{id}} to get the parameter of an input like the name, id and value
you can use {{value}} to get the value of the input
in javascript twig template, you can customize your component
{% block number_radio_javascript %}
<script type="text/javascript">
var selectedInput = $('input[name=briefing[budget][selected]]:checked', '#myForm').val()
if(selectedInput ==1){
//do something
}else{
//do something else
}
</script>
{% endblock %}
You ll need to add some code to include the javascript part. Have a look to "genemu form" to learn how to do, but basicaly : you need to create a twig extension
https://github.com/genemu/GenemuFormBundle/blob/master/Twig/Extension/FormExtension.php
then use the extension each time to use your formType
{% extends '::base.html.twig' %}
{% block jsscript %}
{{ vendor_type_javascript(form) }}
{% endblock %}
{% block content %}
{{ form_widget(edit_form) }}
{{ form_errors(edit_form) }}
{{ form_rest(edit_form) }}
{% endblock %}