Access unmapped form data in form event - symfony

I want to create a dependend select box: if the first selectbox is selected, the second selectbox should be filled.
My first selectbox isn't mapped to the model. I set the value manually in my controller with $form->get('emailTemplateBlockType')->setData($emailTemplateBlockType) .. How can I use this data in my form event to create my second selectbox?
$builder
->add('emailTemplateBlockType', 'entity', array(
'class' => 'MbDbMVibesBundle:EmailTemplateBlockType',
'property' => 'name',
'mapped' => false,
'empty_value' => 'Choose a block type',
'attr' => array(
'class' => 'emailTemplateBlockTypeSelect',
)
))
->add('save', 'submit');
$builder->addEventListener(FormEvents::POST_SET_DATA, function (FormEvent $event) {
$form = $event->getForm();
$data = $event->getData();
echo "name: ".$form->get('emailTemplateBlockType')->getData()->getName();
die;
});
I have an onChange event with jQuery that posts the choice of the first selectbox to my frontcontroller again. The front controller then creates the form again, but this time with the value of the first selectbox added. But since it's not a submit, I think using the POST_SUBMIT event will not work either.
Snippet from my controller:
$form = $this->createForm(new EmailTemplateSiteEmailTemplateBlockType(), $entity, array(
'action' => $this->generateUrl('_email_tpl_site_block_edit', array(
'emailTemplateSiteId' => $emailTemplateSiteId,
'emailTemplateSiteBlockId' => $emailTemplateSiteBlockId,
))
));
if ($request->request->get('blockTypeId')) {
$this->get('logger')->debug('setting block type');
$emailTemplateBlockType = $em->getRepository('MbDbMVibesBundle:EmailTemplateBlockType')
->find($request->request->get('blockTypeId'));
if ($emailTemplateBlockType)
$form->get('emailTemplateBlockType')->setData($emailTemplateBlockType);
else
throw new $this->createNotFoundException('Cannot find blocktype with id '.$request->request->get('blockTypeId'));
}
$form->handleRequest($request);

I think I finally nailed it. I'll describe my pitfalls here, for a full article on how I eventually implemented this see Forms in Symfony2: dependent selectboxes
First of all, it seems I have to submit the full form in order to trigger the form event PRE_SUBMIT. I couldn't just post one variable to the form.
Second, I totally missed that inside the PRE_SUBMIT event, the data is stored in an array instead of an object, which was actually perfectly mentioned in this post. So I should have used:
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($addEmailTemplateBlock) {
$form = $event->getForm();
$data = $event->getData();
if (array_key_exists('emailTemplateBlockType', $data)) {
$addEmailTemplateBlock($form, $data['emailTemplateBlockType']);
}
});
Third is that my unmapped form element can't be accessed in PRE_SET_DATA, but it can in POST_SET_DATA.
Fourth is that I was having issues when I would change the first selectbox, if I had already selected the first and second. This would make sense, because the value in the second selectbox would indeed be invalid, if the first selectbox would change. The easiest way to solve this was to just set the value to empty in the change event of the first selectbox.
I would also like to point out that this approach doesn't require any additional scripting in the controller or javascript when you add more dependent fields. All logic for this is done inside the form builder, so I think it creates better reusable code than the approach of Airam.

I've wrote this small static method:
public static function getUnmappedData($form): array
{
$fields = array_filter($form->all(), function($field)
{
$config = $field->getConfig();
$options = $config->getOptions();
$zval = (true == $options['mapped']);
return($zval);
});
$zval = array_map(function($field)
{
$zval = $field->getData();
return($zval);
}, $fields);
return($zval);
}

Related

Symfony 4 DateType removed disabled with jquery but data not submitted

I have a DateType field on a form that is set to disabled=true . When a specific checkbox is checked by the user some jquery picks it up and removes the disabled property from the html but the value still doesn't get submitted by the form.
Just wondering if there is a better way to accomplish this in symfony? The date field needs to be disabled for the user unless they check the checkbox. The field is added like this:
$builder->add('overrideDate',DateType::class,[
'required'=>false,
'label'=>'Override Date',
'disabled'=>true
]);
I did try changing it via a SUBMIT event so that symfony would recognise the field as enabled since the html doesn't have the disabled anymore the field should still be getting submitted so i'm assuming that it's symfony ignoring it because of the original disabled =true ?
Here is part of the builder, i am trying to change the date field when it's submitted to disabled = false but the overrideDate is still null
$builder->add('overrideDates',CheckboxType::class,[
'label'=>'Override Dates',
'required'=>false
]);
$builder->add('overrideDate',DateType::class,[
'required'=>false,
'label'=>'Override Date',
'disabled'=>true
]);
$builder->addEventListener(FormEvents::SUBMIT,function(FormEvent $event){
$form = $event->getForm();
$data = $form->getData();
if($data->isOverridingDates()){
$form->add('overrideDate',DateType::class,[
'required'=>false,
'label'=>'Override Date',
'disabled'=>false
]);
}
});
SOLUTION
Ok this feels a bit clunky, but it works for what i want to do:
// event listener to enable/disable the overrideDate field if overrideDates === true
$builder->addEventListener(FormEvents::PRE_SUBMIT,function(FormEvent $event){
// get form and data
$form = $event->getForm();
$data = $event->getData();
// add the overridedDate field back into the form
$form->add('overrideDate',DateType::class,[
'required'=>false,
'label'=>'Override Date',
'disabled'=>((bool)$data['overrideDates'] === true ? false : true)
]);
});
You should take a look here : https://symfony.com/doc/current/form/dynamic_form_modification.html
It's the good way to do what you wan to do ! You should use FormEvents::PRE_SUBMIT instead of FormEvents::SUBMIT
// event listener to enable/disable the overrideDate field if overrideDates === true
$builder->addEventListener(FormEvents::PRE_SUBMIT,function(FormEvent $event){
// get form and data
$form = $event->getForm();
$data = $event->getData();
// add the overridedDate field back into the form
$form->add('overrideDate',DateType::class,[
'required'=>false,
'label'=>'Override Date',
'disabled'=>((bool)$data['overrideDates'] === true ? false : true)
]);
});
I have a simpler solution, you have to use the HTML attribute "readOnly". This attribute enables the same function as "disabled" but when symfony get your form data, it takes the "readonly" input into account.
You can add your input in your form builder like this :
$builder->add('overrideDate',DateType::class,[
'required'=>false,
'label'=>'Override Date',
'attr' => ['readonly' => true],
]);

How can I set a value in Twig when the select is built using EntityType?

In a Form say I have a builder option like this:
->add('choice', ChoiceType::class, [
'choices' => [
'Cheese' => 'cheese',
'Plain' => 'plain
]
])
And let's say we are editing this option, in the database they've already selected plain. With twig we can write the widget like this:
{{ form_widget(BurgerForm.choice, {
'value': burger.type
}) }}
This will make the value in the database the pre-selected value for the select. But if you do the same thing with EntityType:
->add('choice', EntityType::class, [
'class' => 'AppBundle:BurgersTypes',
'choice_label' => 'type'
])
And you use the same twig it doesn't pre-select the option from the database. How can I get the value from the database to show as the pre-selected value of the widget?
Pre-selecting a value for this form means setting a value on the underlying data. In your case, the controller ought to look something like:
// src/AppBundle/Controller/MyController.php
namespace AppBundle\Controller\MyController;
use AppBundle\Entity\Order;
use AppBundle\Entity\BurgersTypes;
use AppBundle\Form\Type\FormType;
use Symfony\Component\HttpFoundation\Request;
public function formAction(Request $request)
{
$obj = new Order();
$defaultBurgerChoice = new BurgersTypes('cheese');
$ob->setChoice($defaultBurgerChoice);
$form = $this->create(FormType::class, $obj);
$form->handleRequest($request);
...
// Now, if the form needs to render a value for `choice`,
// it will have the value of BurgerForm.choice determined
// intentionally - by your default, or overridden and
// handled in the request!
return [
'BurgerForm' => $form->createView()
]
}

Symfony2 dynamically assign attr to form field

I'm in a situation where I want to be able to dynamically set the required=true/false option or the array(...other stuff..., 'class' => ' hidden') of a form field.
The context is the following: there is a "Project" entity with some fields. When I create other entities with forms I want to check some attributes of the Project entity and make decisions on the visibility/requiredness of certain fields of the entity I'm creating.
For example if a Project is with attribute "timePressure=high" then a field of a given entity is not required (but is visible). If there are other conditions it becomes invisible etc...
So basically I was hoping to call a function inside each ->add() of the form builder to spit out the relevant portions (e.g. that function would return a string with "required=true" or the other related to the hidden class). The thing is that the function should take as arguments: the projectID (ok, this can be passed as options of the form builder), the class and the field we are talking about to decide. I was envisioning something like:
->add('background', 'textarea', array('attr' => array('rows' => 4, functionToDecideIfInvisible($projectId)), functionToDecideRequiredness($projectId)))
The two function would return the string 'class' => ' hidden' and required=true (or false)
I'd like to avoid to having to specify the field name (in this case background) to avoid code repetition.
Can this be done?
Other suggestions on how to solve the thing?
Thank you!
SN
What you need are Form Events: http://symfony.com/doc/current/cookbook/form/dynamic_form_modification.html#cookbook-form-events-underlying-data
They allow you to modify your form based on your data.
You create you project form in the controller:
$form = $this->createForm(new ProjectType(), $project, array(
'action' => $this->generateUrl('project.edit'),
'method' => 'POST',
));
Then you add the FormEvents::PRE_SET_DATA listener:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$project = $event->getData();
$form = $event->getForm();
// check timePressure on the Project object
if ('high' === $project->timePressure) {
$form->add('timePressure', 'text', array(
'required' => false
);
}
});
}
I found a way to do it.
My add would be like:
->add('background', 'textarea', array_merge($this->vr->fieldReq(get_class($this),
'background', $projectID), array('attr' => array_merge(array('rows' => 4, ),
$this->vr->fieldCssClass(get_class($this), 'background', $projectID) ) )))
To do that I had to define the form as service, plus I created another class as service (the one which holds the two methods I need).
This is the class:
class FieldReqAndCssClass
{
public function fieldReq($parentEntity, $field, $projectID)
{
$required = array();
$required['required'] = false; //do stuff on the database instead of setting it to false hardcoded
return $required;
}
public function fieldCssClass($parentEntity, $field, $projectID)
{
$cssClass= array();
//do stuff on the database instead of setting the class as hidden hardcoded
//$cssClass['class'] = ' hidden';
$cssClass['class'] = '';
return $cssClass;
}
}
Of course in my form I had to:
public function __construct(\AppBundle\Util\FieldReqAndCssClass $fieldReqAndCssClass)
{
$this->vr = $fieldReqAndCssClass; // vr stands for visibility and requiredness
}
And these are the two services:
app_bundle.field_req_and_css_class:
class: AppBundle\Util\FieldReqAndCssClass
arguments: []
app_bundle.form.type.projectdetail:
class: AppBundle\Form\Type\ProjectDetailFormType
arguments: [#app_bundle.field_req_and_css_class]
tags:
- { name: form.type, alias: ProjectDetail }
Of course here in the first service I'll need to inject the entity manager and add it to the construct, and probably also in the form service, but the basic skeleton is working :)
I'm a happy man :)
EDIT
The only problem of the above is that it makes hidden the widget but not the label. To fix it:
->add('background', 'textarea', array_merge($vr->fieldReq($myClass,
'background', $project), array('label_attr' => $vr->fieldCssClass($myClass,
'background', $project),'attr' => array_merge(array('rows' => 4, ),
$vr->fieldCssClass($myClass, 'background', $project) ) )))
Obviously before I have to declare:
$myClass = get_class($this);
$vr = $this->vr;

datagrid filter for relation object as text field (insted of dropdown) in sonata admin in symfony 2.4

I have entity 'Action' with relation to 'User'. Created Admin CRUD controller in SonataAdminBundle. Everything works fine except user filter is rendered as dropdown list. I have 8k user count and growing so you must see why this is a problem.
I want user filter to be text input and on submit to search with LIKE %username%
Right now I add user filter like this - $datagridMapper->add('user').
I know I can add filter type and field type but I am not able to find the right combination and options. Found information on http://sonata-project.org/bundles/doctrine-orm-admin/master/doc/reference/filter_field_definition.html but still no success.
Final solution
Following Alex Togo answer I used this code:
$datagridMapper->add('user', 'doctrine_orm_callback', array(
'callback' => function($queryBuilder, $alias, $field, $value) {
if (empty($value['value'])) {
return;
}
$queryBuilder->leftJoin(sprintf('%s.user', $alias), 'u');
$queryBuilder->where('u.username LIKE :username');
$queryBuilder->setParameter('username', '%'.$value['value'].'%');
return true;
},
'field_type' => 'text'
))
I needed something like this on a project. I implemented this feature using this. You can try to set the 'field_type' option to 'text' (I used 'choice' in the project I worked at) and add the querybuilder actions you need to filter.
Use doctrine_orm_choice option.
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper->add(
'module',
'doctrine_orm_choice',
[],
'choice',
[
'choices' => $this->filterModuleList
]
)
....

Adding custom option at form field type 'entity'

In my form builder, using an entity choice field I can retrieve the contents of an entity by:
$builder->add('manufacturer', 'entity', array(
'class' => 'Manufacturer'....
Everything works fine and the selectbox is rendered correctly at the view. However, I would like to add an extra option at the selectbox called "Add new" (it will not be mapped to an entity), which would result at a select box with options of manufacturers plus one at the end with Add new. What is the best Symfony2 way to achieve that?
public function finishView(FormView $view, FormInterface $form, array $options)
{
$new_choice = new ChoiceView(null, 'value', 'label');
$view->children['manufacturer']->vars['choices'][] = $new_choice;
}

Resources