Create form based on EntityType with option to add quantity - forms

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 %}

Related

Customize data-prototype attribute in Symfony 2 forms

I am trying to follow the Symfony 2.7 docs to create Embed a Collection of Forms using a custom Collection Prototype.
Problem is, that I am not able to create a custom collection prototype as described in the docs.
As in the example there are two simple classes:
A Task class that manages the description of the task and additionally any number of tags, represented by its own Tag class
class Task {
protected $description;
protected $tags;
public function __construct() {
$this->tags = new array();
}
// Getter & Setter for description + additional addTag & removeTag methods
// ...
// Tags getter
public function getTags() {
return $this->tags;
}
}
class Tag {
protected $name;
// ... setName(...), getName()...
}
These are the custom form types:
class TaskType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('description');
$builder->add('tags', 'collection', array('type' => new TagType()));
}
public function getName() {
return 'task';
}
// ...
}
class TagType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('name');
}
public function getName() {
return 'tag';
}
// ...
}
Twig file to render the form
{{ form_start(form) }}
{# render the task's only field: description #}
{{ form_row(form.description) }}
{# render tags - use table instead of ul as in example #}
<div class="table-responsive">
<table class="table">
<thead>
<th>{{ 'task.tag.headline'|trans }}</th>
</thead>
<tbody class="tags-container" data-prototype="{{ form_widget(form.tags.vars.prototype)|e('html_attr') }}">
{{ form_row(form.rules) }}
</tbody>
</table>
</div>
{{ form_end(form) }}
This works fine and renders the tags list inside the table. However, this code uses the default prototype, that (of course) does not create table rows for the different tags.
I tried to add the code to use a custom prototype as described in the docs. How ever the docs does not say anything about where to add this code or how to use it:
Twig code WITH custom prototype code
{{ form_start(form) }}
{# Custom Prototype Code from docs #}
{% form_theme form _self %}
{% block _tags_entry_widget %}
<tr>
<td>{{ form_widget(form.name) }}</td>
</tr>
{% endblock %}
{# render the task's only field: description #}
{{ form_row(form.description) }}
{# render tags - use table instead of ul as in example #}
<div class="table-responsive">
<table class="table">
<thead>
<th>{{ 'task.tag.headline'|trans }}</th>
</thead>
<tbody class="tags-container" data-prototype="{{ form_widget(form.tags.vars.prototype)|e('html_attr') }}">
{{ form_row(form.rules) }}
</tbody>
</table>
</div>
{{ form_end(form) }}
Using the custom prototype code like this results in the error:
Method "name" for object "Symfony\Component\Form\FormView" does
not exist in "MyAppBundle:Task:task.html.twig"
This sounds reasonable, since name belongs to the Tag class and not to the Task class.
Problem 1: How to use/access the Tag form inside the template?**
I removed <td>{{ form_widget(form.name) }}</td> from the prototype template and replaced it with <td>Test</td> to see if the template is used. The result: The template is NOT used and has no effect.
Problem 2: What is the correct way to set/activate the prototype template?
I found other threads dealing with prototype question/problems. The answers propose different solutions using macros, external twig files, etc. Since the Symfony docs seems to offer a solution within the same file without using hacks like macros, I would like to know implement this solution.
This is working for me - I am sorry - it is Symfony 3, but you should be able to translate.
In the buildForm method of my PersonType class, I have a CollectionType of emails
->add( 'emails', CollectionType::class, [
'label' => 'common.email',
'entry_type' => AppEmailType::class,
'by_reference' => true,
'required' => false,
'label' => false,
'empty_data' => null,
'allow_add' => true,
'allow_delete' => true,
'delete_empty' => true,
'mapped' => false,
'prototype_name' => '__email__'
] )
The template to render the PersonType includes the emails form like so
{% include 'common/emails.html.twig' with {'form': form.emails } %}
common/emails.html.twig
This template is the container for the collection
<div class="emails">
<span class="sub-form-legend">{{'common.email'|trans}}</span>
{{form_row(form)}}
{% if form.vars.allow_add %}
<div class="add-one-more-row">{{ 'common.add_one_more'|trans}}</div>
{% endif %}
</div>
In fields.html.twig, I have an entry_row template specific to the form, which uses a common email template defined in the same file.
{% block _user_person_emails_entry_row %}
{{ block('_emails_entry_row') }}
{% endblock %}
{% block _emails_entry_row %}
{% spaceless %}
<div class="form-row email">
<span class="type-select">{{ form_widget(form.type) }}</span>
<span class="input">{{ form_widget(form.email) }}</span>
<span class="comment">{{ form_widget(form.comment) }}</span>
<span class="remove-form-row" title="{{'common.remove'|trans}}" id="email-__email__"><i class="fa fa-remove"></i></span>
</div>
{% endspaceless %}
{% endblock %}
To find out the name, trace the names of your forms and prefix them with an underscore. Use {{dump(form)}} to get the unique name.
The HTML will be placed in the data-prototype attribute.

Symfony form how to add class to form group

Using:
->add('name', TextType::class, [
'label' => 'Name',
'required' => true,
'attr' => [
'class' => 'myclass'
],
])
I'm adding the class "myclass" to the <input> element, getting:
<div class="form-group">
<label ...>Name</label>
<input type="text" required="required" class="myclass" ...>
</div>
What if I wanted to add the "myclass" class not to the input itself, but to its <div class="form-group"> container, in order to get:
<div class="form-group myclass">
<label ...>Name</label>
<input type="text" required="required" ...>
</div>
What's the best way to accomplish that?
I actually found a way of doing it in PHP (Symfony 4.4.2). Class has to go into 'row_attr' instead of 'attr'. For the given example it'd be:
->add('name', TextType::class, [
'label' => 'Name',
'required' => true,
'row_attr' => [
'class' => 'myclass'
],
])
related docs: https://symfony.com/doc/current/reference/forms/types/text.html#row-attr
In my case I needed to override the form_row block:
{% block form_row -%}
<div class="form-group myclass{% if (not compound or force_error|default(false)) and not valid %} has-error{% endif %}">
{{- form_label(form) -}}
{{- form_widget(form) -}}
{{- form_errors(form) -}}
</div>
{%- endblock form_row %}
as suggested, the docs helps to understand why.
As #Yoshi pointed out, in the form definition itself you cannot add classes to the container div.
The right way to customize form rendering is to override the base block via Twig as explained here: http://symfony.com/doc/current/cookbook/form/form_customization.html#how-to-customize-an-individual-field (BTW I suggest you to read carefully the whole article to know everything you need to know about customizations).
In the overridden block you have also the widget data if you want e.g. to add a class with the field id:
{% block _product_name_widget %}
<div class="text_widget {{ id }}">
{{ block('form_widget_simple') }}
</div>
{% endblock %}
in this example above, {{ id }} will be replaced with the widget (field) id.

zend 2 -using Zend Forms in partial view helper

I have a form that is repeated over several different modules.
I would like to avoid repeating the same process over and over again and want to render the form once (in my application module) and then simply use a partial view script to call it wherever I wish like this;
<?php echo $this->partial('partialLayout/advertForm'); ?>
Following the advice here it would be pretty straightforward to do this if I simply created a HTML form in the partial script and then called it.
However, I want to use Zend Forms. Bearing in mind that zend form are loaded onto the page via the controller- how do I get around this.
i guess that the crux of the question is - how do you call a Zend Form in another module using a partial view script.
I did something like that with a ViewHelper. My solution is posted here, in Zend Forums.
There it is.
The code ViewHelper :
<?php
/**
* View helper to return the HTML code of the login form.
*
* #filesource LoginForm.php
* #encodage UTF-8
* #author DAFAP Informatique - Alain Pomirol
* #date 10 nov. 2015
* #version 2015-1
*/
namespace Login\View\Helper;
use Zend\View\Helper\AbstractHelper;
use Zend\ServiceManager\ServiceLocatorAwareInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\View\Model\ViewModel;
use Login\Form\LoginForm as Formulaire;
class LoginForm extends AbstractHelper implements ServiceLocatorAwareInterface
{
protected $sm;
public function setServiceLocator(ServiceLocatorInterface $serviceLocator)
{
$this->sm = $serviceLocator;
return $this;
}
public function getServiceLocator()
{
return $this->sm;
}
public function __invoke()
{
$form = new Formulaire();
$layout = new ViewModel(array(
'form' => $form
));
$viewRender = $this->getServiceLocator()->getServiceLocator()->get('ViewRenderer');
$layout->setTemplate('login/index/index.phtml');
return $viewRender->render($layout);
}
}
The statement in module.config.php in Login module :
'view_helpers' => array(
'invokables' => array(
'loginForm' => 'Login\View\Helper\LoginForm'
)
),
Use in view of the application module :
'view_helpers' => array(
'invokables' => array(
'loginForm' => 'Login\View\Helper\LoginForm'
)
),
It's a common problem! Forms can really be a slow-down with ZF2.
I use Twig (recommended) but you can port this approach to plain old ZF2 templates too.
Here's one such template I use nearly everywhere:
horizontal_form.twig
{# render rows only, not the form tag #}
{% do form.prepare() %}
{% for f in form %}
{% do f.setOption( 'twb-layout', 'horizontal' ) %}
{{ formRow( f ) }}
{% endfor %}
I've got a second with a slightly different approach to support a specific IdentifiableForm in my code:
identifiable_form.twig
<form id="{{ form.getAttribute('name') }}" name="{{ form.getAttribute('name') }}" action="{{ form.getAttribute('action') }}" class="form-horizontal" role="form">
<input type="hidden" name="class" value="{{ form.getAttribute('class') }}">
{% for f in form %}
{% do f.setOption( 'twb-layout', 'horizontal' ) %}
{% do f.setOption( 'twb-form-group-size', 'form-group-sm' ) %}
{{ formRow( f ) }}
{% endfor %}
<div class="hr-line-dashed"></div>
<button type="submit" class="btn btn-primary ladda-button" data-style="expand-right">Save</button>
</form>
With those two in the pocket, my usage looks like this:
In a twig template that should use a form
...include the template from the other:
<div class="panel-body">
{% set form = general_config_form %}
{% do form.prepare() %}
{% include 'application/generic/identifiable_form' %}
</div>
The controller action that operates that template looks like:
$vm = new ViewModel();
$vm->setTerminal( true );
$vm->setTemplate( 'application/admin/config/index' );
$sm = $this->getServiceLocator();
$fm = $sm->get( 'FormElementManager' );
$country = $config_mapper->get('general.country', true );
$general_config_form = $fm->get( GeneralConfigForm::class, [ 'country' => $country ] );
$general_config_form->setAttribute('action', '/admin-config/form-save' );
$vm->setVariable( 'general_config_form', $general_config_form );
In the end it's just a game of:
instantiating/loading the form in the action
setting the form as a variable in the ViewModel
including the "generic form template" from that action's actual template
On topic, if you hate rigging forms as much as I do, I made a little tool to save time here: https://github.com/Saeven/zf2-circlical-formtool
Second, if you use Bootstrap in your app, this form helper replacement really makes things look nice: https://github.com/neilime/zf2-twb-bundle
Good luck!

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 %}

Get variable value in Symfony2 FormBuilder

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()),
));