Symfony2 dynamically assign attr to form field - symfony

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;

Related

Select2 with Symfony with custom tags

The senario I have is that I want a select2 dropdown which allows users to select multiple options. But when they select an option I want to have the selectbar to show an abbreviation of the selected item. Right now I have an key value array where the key is the abbreviation and the value is the full-text.
Right now I can't find a way of getting the "original" key of my array when I select an option. Nor do I see anything that can relate to the key of the array.
Form class
$builder->add('relations', ChoiceType::class, [
'choices' => [
'isp' => 'Internet service provider',
'db' => 'Database'
]);
View
{{ form_row(form.relations) }}
Javascript
<script>
$(document).ready(function() {
$('#form_relations').select2({
closeOnSelect : false,
placeholder : "Placeholder",
allowHtml: true,
allowClear: true,
multiple: true
});
});
</script>
Right now it will simply use the value of the array when you select an option, however I'd like to use the abbreviation. I tried logging the selected options but this does not seem to contain the key of the array. I also tried looking into the array which symfony is returning to the view and it seems to replace the keys with an incremental index instead of keeping my own. Is there a way of making sure symfony will keep my own keys and use those when an option is being selected with select2JS?
The solution should be to set an additional data-attribute to your options. To do this you should use an object to set your choices. So let's add a new Option class first:
namespace App\Model;
class MyOption
{
private $name;
private $abbreviation;
public function __construct($name, $abbreviation) {
$this->name = $name;
$this->abbreviation = $abbreviation;
}
function getName() {
return $this->name;
}
function getAbbreviation() {
return $this->abbreviation;
}
}
Now create your dropdown in your formType like this:
use App\Model\MyOption;
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('relations', ChoiceType::class, [
'choices' => [
new MyOption('Internet service provider', 'isp'),
new MyOption('Database', 'db'),
],
'choice_label' => function(MyOption $option, $key, $value) {
return $option->getName();
},
'choice_attr' => function(MyOption $option, $key, $value) {
return ['data-abbreviation' => $option->getAbbreviation()];
},
]);
}
The result would be something like:
<select id="product_option_relations" name="product_option[relations]">
<option value="0" data-abbreviation="isp">Internet service provider</option>
<option value="1" data-abbreviation="db">Database</option>
</select>
Next you will be able to get the selected abbreviation with for example jQuery:
$("#product_option_relations").find(':selected').data('abbreviation');

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()
]
}

Set the class of all Input Boxes in Yii2

I have a web application designed in Yii2. The client wants all the text boxes to be shorter/smaller. In bootstrap, in addition to "form-control", I'd need to add the "input-sm" class as well.
However, as far as Yii2 is concerned, the form-control class is set in the model. Example: ActiveField model's has
'inputOptions' => [
'class' => 'form-control',
],
However, I am using various widgets and extensions and need a supported solution to set the class such that with a few edits, I can make all text boxes and other controls look smaller. I don't want to mention a custom inputOptions in every form.
One solution could be to extend all the models I use and setting the class (input-sm) there, but would prefer a CSS or simpler solution.
Extend Your Active Field Class as:-
<?php
namespace auction\widgets;
use yii\helpers\ArrayHelper;
class ActiveField extends \yii\widgets\ActiveField{
//Error Options For Active Field Error Tag
public $errorOptions= ['class' => 'error', 'tag' => 'span'];
public function init(){
//Changing Input Options Merge with form-class
$this->inputOptions = ['placeHolder' => $this->model->getAttributeLabel($this->attribute), 'class' => 'input-sm'];
parent::init();
}
/**
* #param null $label Setting Label Value to false
* #param array $options
*/
public function label($label = null, $options = [])
{
$this->parts['{label}'] = '';
}
}
now use ActiveForm As:-
<?php $form = ActiveForm::begin([
'id' => 'login-form',
'fieldClass' => 'auction\widgets\ActiveField',
'successCssClass' => false,
'options'=> ['role' => 'form']]); ?>

Render form without value in Symfony2

I'm building a form which have default value in some fields:
$builder->add('field', 'text', array('data' => 'Default value');
Then render this form field in Twig like:
{{ form_widget(form.field) }}
It's worked OK, but I don't want 'Default value' set in input tag rendered in HTML, because I only want this default value set internal that End-user doesn't aware about this value. Is there any built-in method in Symfony2 to handle it or I have to make some custom code?
You could modify your entity in order to do this:
class MyEntity{
const DEFAULT_FOO = "Default value";
// ...
private $foo;
// ...
public function setFoo($foo){
if ( $foo === null ){
$foo = self::DEFAULT_FOO;
}
$this->foo = $foo;
}
// ...
}
And then make sure that you set by_reference in order to ensure setter is being invoked each time:
$builder->add('field', 'text', array(
'by_reference' => true
));
As far i understand
try to add required false and handle it in your controller action.
$builder->add('field', 'text', array('required' => false));

Access unmapped form data in form event

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);
}

Resources