Get variable value in Symfony2 FormBuilder - forms

I am building a form in Symfony2 and for some rows I want to pass a formatted version of each widget's value as a title attribute which I can then display in another div.
If I have not set a formatted version, I will just insert the actual, non-formatted value into the div instead.
Ideally, my twig code might look like this:
{% block form_row %}
{% spaceless %}
<div class="field-display-value">
{% if attr['title'] is defined %}
{{ attr['title'] }}
{% else %}
{{ form.vars.value }}
{% endif %}
</div>
<div class="field-widget">
{{ form_widget(form) }}
</div>
{% endspaceless %}
{% endblock form_row %}
But I don't know how I can access the value of the widget in the formBuilder. Ideally I would like something like this:
$builder->add('some_field', 'text', array(
'attr' => array('title' => someFormattingFunction( this.widget.value ),
));
Obviously, this.widget.value pseudocode doesn't work.
Don't know if even possible, but I don't want to have to resort to javascript madness.
Any ideas?

See this documentation :
http://symfony.com/doc/current/cookbook/form/dynamic_form_modification.html#dynamic-generation-for-submitted-forms
You have to set a listener for the PRE_SET_DATA event, on your formBuilder.
Then you will be able to access the data passed to this form with :
$data = $event->getData();
Now you juste have to add your fields inside.
/!\ Your fields will only be added, if an object is passed to the form.
So if you still want the fields when there is no data passed, you have to add the fields outside the event lister function, and then modify the fields attribute title inside this function.
$form = $event->getForm();
$form->add('some_field', 'text', array(
'attr' => array('title' => someFormattingFunction($data.getSomething()),
));

Related

Symfony - Using collectionType length as counter in prototype twig template doesn't work

I tried a lot, but I can't get this to work.
I'm using a Form with a CollectionType (ChoiceType) in Symfony 4.
In my form, you can add children (literally children - small young people :) - it is an age choice field).
For this, I added one default child.
In my form row, I want to use ids, label etc with the current number of the child.
e.g. I want the label of each child to be like "Child 1", "Child 2" and so on.
First the code:
ChildField in FormType
->add('childs', CollectionType::class, [
'allow_add' => true,
'prototype' => true,
'entry_type' => ChoiceType::class,
'entry_options' => [
'choices' => [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18],
],
'data' => [
1 => 0,
]
]);
My template for the field row
<div class="form-group row" id="childsupport_child_{{child_number}}_formgroup">
<div class="col-sm-12" id="childsupport_child_{{child_number}}_toplabel"><b>{% trans %}childsupport.child{% endtrans %} {{child_number}}</b></div>
<div class="col-sm-8" id="childsupport_child_{{child_number}}_label" for="childsupport_childs_{{child_number}}">{% trans %}childsupport.childAge{% endtrans %}</div>
<div class="col-sm-2">
{{ form_widget(childField) }}
</div>
{% if child_number > 1 %}
<div class="col-sm-1" id="childsupport_child_{{child_number}}_delete"><span class="btn btn-danger" onclick="delete_child({{child_number}})">X</span></div>
{% endif %}
</div>
As you can see, I want to use the variable {{child_number}} in this template.
I'm using the template via macro.
[...]
{% import _self as formMacros %}
[...]
{% macro printChildRow(childField, counter) %}
{% include 'childsupport.child.html.twig' with {'childField': childField, counter} %}
{% endmacro %}
[...bla bla form starts, other field get printed...]
<div class="js-child-wrapper" data-prototype="{{formMacros.printChildRow(childsupport_form.childs.vars.prototype, childsupport_form.childs|length+1)|e('html_attr') }}"
data-index="{{ childsupport_form.childs|length }}"
></div>
{% for childField in childsupport_form.childs %}
{% set counter = ( counter | default(0) ) + 1 %}
{{ formMacros.printChildRow(childField, counter) }}
{% endfor %}
[...bla bla submit button, form ends...]
I'm trying to pass the current collection length+1 as counter.
An at last the js.
function add_child(){
var wrapper = $('.js-child-wrapper');
var prototype = wrapper.data('prototype');
var index = wrapper.data('index');
var newChild = prototype.replace(/__name__/g, index+1);
$(newChild).insertAfter( "#childsupport_child_"+index+"_formgroup" );
wrapper.data('index', index + 1);
var i;
for (i = 0; i <= index; i++) {
$('#childsupport_child_'+i+'_delete').hide();
}
}
This works for the first time.
childsupport_form.childs|length+1 is 2 then and the second child is correctly added.
But then it doesn't work anymore.
So I got the feeling, that childsupport_form.childs|length is not a dynamic value that changes in the process of the addition of CollectionFields.
What do I need to pass instead as second variable for the prototype in this line?
<div class="js-child-wrapper" data-prototype="{{formMacros.printChildRow(childsupport_form.childs.vars.prototype, childsupport_form.childs|length+1)|e('html_attr') }}"
data-index="{{ childsupport_form.childs|length }}"
></div>
Or am I completely on the wrong way?
Thanks in advance!
Temporary, I used the quick and dirty solution and cloned the childsupport.child.html.twig to childsupport.child_prototype.html.twig, where I used name instead of {{child_number}}.
So, I used the first template for the initial creation of the predefined collection values and the cloned one for the onClick Event.
Works.
But still quick and dirty.

Create form based on EntityType with option to add quantity

I'm trying to setup a Symfony form that will allow a user to select a number of elements adding the wished quantity. I would like to be able to have a FormType which would be somewhere between an EntityType and the IntegerType. Meaning I need to have a list of elements based on a query_builder to select only part of my products, but I don't just want to be able to select the product but say how many I want of a given number of products.
I've been able to create a form based on the options I send. For each product in my array I add an IntegerType field to my form using the builder. This allows me to show a list of products and ask the client the number of elements he wants.
Now the problem I have is adding detailed data from the product in the list as I don't know the forms field names I can't interact with the label. If I could add something allowing me to say that the 'label' could show as raw html, I could concatenate the wished data in the label.
Here is my current form:
class OfferRequestStepMultipleHardwareType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
// instead of haveing set fields I create them based on the $option['step']->getProducts() data
foreach ($options['step']->getProducts() as $product){
$builder->add($product->getId().'-qty', IntegerType::class, [
'mapped' => false,
'attr' => [
'value' => 0,
'class' => 'longlist',
'min' => 0
],
'row_attr' => [
'class' => 'longlist'
],
'label' => $product->getNumber() // ideally I would do some thing like '<div>'.$product->getNumber().'</div><div>'.$product->getDescription().'</div>' and then show it as raw in the form template
]);
}
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => OfferRequest::class,
'step' => StepHardware::class,
]);
}
}
The problem is this generates code a bit like this:
<div id="offer_request_step_multiple_hardware">
<div class="longlist"><label for="offer_request_step_multiple_hardware_27-qty" class="required">Product 1</label><input type="number" id="offer_request_step_multiple_hardware_27-qty" name="offer_request_step_multiple_hardware[27-qty]" required="required" value="0" class="longlist"
min="0"></div>
<div class="longlist"><label for="offer_request_step_multiple_hardware_28-qty" class="required">Product 2</label><input type="number" id="offer_request_step_multiple_hardware_28-qty" name="offer_request_step_multiple_hardware[28-qty]" required="required" value="0" class="longlist"
min="0"></div>
<div class="longlist"><label for="offer_request_step_multiple_hardware_29-qty" class="required">Product 3</label><input type="number" id="offer_request_step_multiple_hardware_29-qty" name="offer_request_step_multiple_hardware[29-qty]" required="required" value="0" class="longlist"
min="0"></div>
<div class="longlist"><label for="offer_request_step_multiple_hardware_30-qty" class="required">Product 4</label><input type="number" id="offer_request_step_multiple_hardware_30-qty" name="offer_request_step_multiple_hardware[30-qty]" required="required" value="0" class="longlist"
min="0"></div><input type="hidden" id="offer_request_step_multiple_hardware__token" name="offer_request_step_multiple_hardware[_token]" value="PZaPfxKNSV-TjftRgjAw1K8XCUr7Dvkrp57kWTMBJ64"></div>
I've also tried to create a form theming in the template to change the way the Integer widget show with this:
{% form_theme form _self %}
{% block question %}
<h1>{{ offer.lastStep.Question }}</h1>
{% endblock %}
{% block integer_widget %}
<div class="name_row">
{{ form_label(form)|raw }}
{{ form_errors(form) }}
{{ form_widget(form) }}
{{ form_help(form) }}
</div>
{% endblock %}
Without any success as when I concatenate and try to add the filter "raw" to it, the code is still changed autoescaped. The problem is then I've found information on how to set a specific label for a specific field of a form, but again I generate the form on the go and have no way of knowing the field names (like described here). Any suggestions on how to have this work?
Ideally I would like to be able to create a FormType based on the EntityType which would allow to add an Integer instead of selecting the Entity elements...
Any help would be nice!
Labels in Symfony forms are escaped at some point before the rendering of the template, so applying the twig raw filter won't prevent html entities from being escaped.
What you can do is create a new twig filter, to apply the php function html-entity-decode (following code untested):
Twig/HtmlDecodeExtension.php
<?php
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
class HtmlDecodeExtension extends AbstractExtension
{
public function getFilters()
{
return [
new TwigFilter('htmldecode', [$this, 'htmlDecode']),
];
}
public function htmlDecode($value)
{
return html_entity_decode($value);
}
}
If you use the default configuration with services autowiring, you don't have to register the new extension; in the other case, do it in config/services.yaml
Now you can use the filter in your form template instead of raw, and put whatever html you like in the label:
{% form_theme form _self %}
{% block question %}
<h1>{{ offer.lastStep.Question }}</h1>
{% endblock %}
{% block integer_widget %}
<div class="name_row">
{{ form_label(form)|htmldecode }}
{{ form_errors(form) }}
{{ form_widget(form) }}
{{ form_help(form) }}
</div>
{% endblock %}

Symfony2 - Collection type is displayed twice in my view

I have a form with a collection and I can see it properly when I use the following syntax:
form_row(form.items)
Then when I try to render it by myself, it is duplicated.
Here is my code in my twig file:
{{ form_errors(form) }}
{{ form_start(form) }}
... some fields here ...
<div id="{{ form.items.vars.id }}" class="collection items" data-prototype="{{ form_widget(form.items.vars.prototype)|e }}">
<h3>{{ form_label(form.items) }}</h3>
Add
{% for item in form.items %}
{{ form_row(item) }}
{% endfor %}
</div>
... some other fields here ...
{{ form_end(form) }}
Here is my code in my form object:
$builder->add('items', 'collection', array(
'type' => new ItemType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'required' => true,
));
Add here is my output:
<!-- This is the one I need, I removed the prototype for the sake of clarity -->
<div id="bundle_data_items" class="collection items" data-prototype="...">
<h3>
<label class="required">Items</label>
</h3>
Add
</div>
<!-- This one is automatically added. The prototype is empty -->
<div>
<label class="required">Items</label>
<div id="bundle_data_items" data-prototype=""></div>
</div>
Do you have any ideas why this happens?
I followed that documentation. Did I miss something?
Edit:
This happens only when my collection is empty
I found the solution to that.
It turns out that when the collection is empty, the rendering of it never occurs. I could use sjagr solution but as there is some fields I want to render automatically, I cannot use it.
So my solution is the following:
{{ form_errors(form) }}
{{ form_start(form) }}
... some fields here ...
<div id="{{ form.items.vars.id }}" class="collection items" data-prototype="{{ form_widget(form.items.vars.prototype)|e }}">
<h3>{{ form_label(form.items) }}</h3>
Add
{% for item in form.items %}
{{ form_row(item) }}
{% else %}
{% do form.items.setRendered %}
{% endfor %}
</div>
... some other fields here ...
{{ form_end(form) }}
When your collection is empty, the for loop in {% for item in form.items %} never gets to execute, hence form_row(item) never happens. This means, to Twig/Symfony, you never actually outputted the field.
Then you do form_end at the end of the form. This is not usually a big deal, but from the docs:
This helper also outputs form_rest() unless you set render_rest to false
So, you must simply pass a false value for render_rest:
{{ form_end(form, {'render_rest': false}) }}
I'm not sure how form_widget did fix your problem, but I'm also not sure how you tried that alternative - perhaps you ran it outside of the for loop.
As an aside, you should consider making your own form.html.twig template file so you can reuse the markup format you've chosen for your collection. It will allow you to simply do form_widget(...) without having to break apart the pieces and your form_end will always consider the field to be outputted even if the collection is empty.
Unfortunately i cannot comment yet (no 50 rep), but perhaps this is caused the browser itself. A rendering issue due to markup someplace? See if the raw http response has the same thing. Since it works with form_row(form.items) but not your own, it "could be" that.
I would check if it's empty also before outputting it.
Similar thing happened to me, label of CollectionType was rendering twice when Collection is empty, but it was rendering once when there are data.
This is my configuration:
$builder->add('items', CollectionType::class, [
'entry_type' => NumberType::class,
'required' => true,
'label' => 'Fields',
'entry_options' => [
'label' => false,
],
'prototype' => true,
])
and in Twig I was displaying Collection label in this way:
{{ form_label(form.items) }} and rendering field widgets in somewhere else. In this case label was displayed twice, once where this code is placed, and the other at the end of form.
Solution is to change it to:
{{ form_row(form.items) }}
or if you need custom rendering for form widgets like in my case, following code will print only labels:
{% if 0 < (form.items|length) %}
{{ form_label(form.items) }}
{% else %}
{{ form_row(form.items) }}
{% endif %}

Placeholder for form_widget choices on Twig

Using PHP on controller side, i can add a placeholder to my field using the createFormBuilder():
$form = $this->createFormBuilder($company)
->add('name', 'text')
->add('cities', 'entity', array(
'class' => 'NexBaseBundle:City',
'property' => 'name',
'placeholder' => 'Choose an option',
))
->getForm();
I am looking to add a placeholder using Twig, for simple fields i can use attr like this :
{{ form_widget(form.name, {'attr': {'placeholder': 'My placeholder'} }) }}
I have tried with this but no luck:
{{ form_widget(form.cities, {'attr': {'placeholder': 'Choose your city'} }) }}
You can't because the "select box" on html5 does not have an attribute "placeholder", your attribute "placeholder" in your formbuilder is an alternative of empty_value
https://github.com/symfony/symfony/issues/5791
On the createFormHandler its the empty_data option. You can use the placeholder option but its possible that you get some trouble in older Browsers.
Normally i disable that HTML5 functions and do it with Javascript.
http://www.hagenburger.net/BLOG/HTML5-Input-Placeholder-Fix-With-jQuery.html
Here is a cool jQuery plugin to fix that problem in older Browsers.
Setting the value from your controller make not really sense. You can set a key in your form and translate them for example.
You can add the attribute only on text fields. Your second method on a Selext-Box is not working because there is not attribute placeholder on that element. So you can use the placeholder in your FormType but that is changed to an option field in background.
Here is an example how I am currently adding placeholders (My select list can be different then yours, but you can play around of this snippet):
{% form_theme cart_form _self %}
{% block choice_widget %}
{% if form.vars.choices is defined %}
{% for choices in form.vars.choices %}
<div class="row">
<div class="col-md-6">
<select name="{{ form.vars.full_name }}" class="form-control">
<option value="__none">--Choose</option>
{% for choice in choices %}
<option value="{{ choice.value }}">{{ choice.label }}</option>
{% endfor %}
</select>
</div>
</div>
{% endfor %}
{% endif %}
{% endblock %}

Symfony2 form layout - variables source

Here's the main form layout twig file:
https://github.com/symfony/symfony/blob/master/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig
An example:
{% block form_widget_simple %}
{% spaceless %}
{% set type = type|default('text') %}
<input type="{{ type }}" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %}/>
{% endspaceless %}
{% endblock form_widget_simple %}
I wonder where varaibles like "type" or "value" come from?
The goal I'm trying to achieve is to set form row's label as a placeholder in the widget. How can I accomplish this?
Details how to override Template for Form field you will find here.
If you trying to change labels to placeholders all you need is to change way of rendering your forms. Remove form_widget(form) and switch to render every separate form field:
{# ... #}
<div class="form-group">
{{ form_errors(form.email) }}
{{ form_widget(form.email, {'attr': {'class': 'form-control', 'placeholder': 'E-mail address'|trans }}) }}
</div>
{# ... #}
This example generate input for email field and html/css classes for bootstrap.
And shows you how {{ type }} and {{ value }} are passed - by attr array.
Good Luck!