Symfony: trying to customize a collection form prototype - symfony

I have this form:
class BillType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('user')
->add('numberPlate')
->add('servicesPerformed', CollectionType::class, array(
'label' => false,
'entry_type' => ServicePerformedType::class,
'allow_add' => true,
))
->add('Save', SubmitType::class)
;
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'DefaultBundle\Entity\Bill'
));
}
being ServicePerformedType class this:
class ServicePerformedType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('description', TextareaType::class, array('label' => false))
->add('price', TextType::class, array('label' => false))
->add('quantity', TextType::class, array('label' => false));
}
}
And this template to render the form:
{{ form(form) }}
Add service
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js"></script>
<script type="text/javascript">
var index = 0;
$('a').on('click', function() {
var prototype = $('#bill_servicesPerformed').data('prototype');
prototype = prototype.replace(/_name_/g, index.toString());
$('#bill_servicesPerformed').html(prototype);
index++;
})
</script>
As it is said in the docs, to get a custom prototype I should add the lines below at the top of my template:
{% form_theme form _self %}
{% block _servicesPerformed_entry_widget %}
I WILL WRITE MY CUSTOM PROTOTYPE HERE
{% endblock %}
But when I press Add service I dont get the text I WILL WRITE MY CUSTOME PROTOTYPE HERE, but the description, fields and quantity related to the ServicePerformedType class as before..
NOTE: maybe there are other ways to custom a form prototype, but I'm interested on this, so will be very thankful to someone who give a solution related to this way to custom form prototypes, thanks.

I must warn you that customizing the prototype could be a bit tricky. If you change your FormType fields you'll need to go through the template and make the same changes as well or your form will fail to render.
What I like to do is to create a custom template for that specific field and then customize it appropriately. So, looking at your code I would do something like this:
Create a page template - the one you'll use to render the entire page, including the form.
{# MyBundle/Resources/views/myPage.html.twig #}
{% extends "::base.html.twig" %}
{# This will tell the form_theme to apply the
`MyBundle:form/fields/servicePerformed.html.twig` template to your "form" #}
{% form_theme form with [
'MyBundle:form/fields/servicePerformed.html.twig'
] %}
{% block body %}
<div>
{{ form_start(form) }}
{{ form_rest(form) }}
{{ form_end(form) }}
</div>
{% endblock %}
Now you'll need to create the template MyBundle/Resources/views/form/fields/servicePerformed.html.twig. It will be used to customize only the servicePerformed field. The template should look something like this
{% macro service_template(fields) %}
<tr>
<td>I WILL WRITE MY CUSTOM PROTOTYPE HERE</td>
</tr>
{% endmacro %}
{#
The block name must match your field name!
You can find it by going into the debug toolbar -> Forms ->
click on your form field and then search for "unique_block_prefix".
Copy that and add "_widget" at the end.
#}
{% block _service_performed_widget %}
<table data-prototype="{{ _self.service_template(form.vars.prototype)|e }}">
{% for fields in form.children %}
{{ _self.service_template(fields) }}
{% endfor %}
</table>
{% endblock %}
I want to note that in the field template I'm passing the original prototype _self.service_template(form.vars.prototype). By doing this, you can use the original fields and render them in your customized prototype. For example this code:
{% macro service_template(fields) %}
<tr>
<td>{{ form_widget(fields.description) }}</td>
</tr>
{% endmacro %}
Will result in something like the following rendered prorotype:
<tr>
<td>
<input type="text" id="service_performed___name___description" name="service[__name__][description]"/>
</td>
</tr>
You can then manipulate it using your javascript.
I hope this helps you.

Actually form theme blocks that start with an underscore _ relate to a field with a specific name.
What I mean is that, if your master form BillType is called my_form, you will need to do this:
{% form_theme form _self %}
{% block _my_form_servicesPerformed_entry_widget %}
I WILL WRITE MY CUSTOM PROTOTYPE HERE
{% endblock %}
The problem with this approach is that it concerns a specific iteration of BillType. If you use this form type elsewhere and provide it with a different name my_form_2, you would have to add an identical block named _my_form_2_servicesPerformed_entry_widget.

You can use macro, have a look at below example it's working fine for me even in Symfony3. Using this example you will be able to format your collection prototype as well.
View
{% macro widget_prototype(widget, remove_text) %}
{% if widget.vars.prototype %}
{% set form = widget.vars.prototype %}
{% set name = widget.vars.name %}
{% else %}
{% set form = widget %}
{% set name = widget.vars.full_name %}
{% endif %}
<div data-content="{{ name }}" class="panel panel-default">
<div class="section row">
<div class="col-md-12">
<label class="field-label">Skill <span class="text-danger">*</span></label>
<label class="field select">
{{ form_widget(form.skill) }}
<i class="arrow double"></i>
</label>
</div>
</div>
<div data-content="{{ name }}">
<a class="btn-remove" data-related="{{ name }}">{{ remove_text }}</a>
{{ form_widget(form) }}
</div>
</div>
{% endmacro %}

Your template to render the form has some problems. The first is this line:
prototype = prototype.replace(/_name_/g, index.toString());
The regex should be __name__.
Next, you are retrieving the prototype, but then immediately overwriting it and replacing the HTML of the prototype. There is nothing there I can see that actually appends the new form to your existing form anywhere. Plus since you just have a string of text, that replace isn't going to find any text __name__ to replace.
You should post the full extent of your Twig/Javascript so we can actually see the #bill_servicesPerformed as well as everything else you are trying to do. Before you write a custom prototype you should get the form working with the standard prototype just to make sure you don't have any bugs there first.

As an example, this is the way I keep going it. I do not know if there are some reasons not to, so be careful.
Form to include prototype:
<div class="modal-body" id="contactMehtods" data-prototype="
{% filter escape %}
{{ include(':organization/partials:prototype_contact_method.html.twig', { 'contact_method': contact_form.contactMethods.vars.prototype }) }}
{% endfilter %}">
<div class="form-group">
{{ form_label(contact_form.name, null, { 'label_attr': { 'class': 'control-label' }}) }}
{{ form_widget(contact_form.name, {'attr': {'class': 'form-control'}}) }}
</div>
</div>
Prototype template:
<div class="form-group">
{{ form_label(contact_method.name, null, { 'label_attr': { 'class': 'col-sm-3 control-label' }}) }}
<div class="col-sm-9">
{{ form_widget(contact_method.name, {'attr': {'class': 'form-control'}}) }}
</div>
</div>
<div class="form-group">
{{ form_label(contact_method.value, null, { 'label_attr': { 'class': 'col-sm-3 control-label' }}) }}
<div class="col-sm-9">
{{ form_widget(contact_method.value, {'attr': {'class': 'form-control'}}) }}
</div>
</div>
But a note to consider, the javascript needs to accommodate for these changes I guess.

Related

Symfony4 Twig template error: Variable "widget" does not exist

I am getting Twig_Error_Runtime
I am trying to find the mistake I've made, but I can't see any... maybe I am looking at the wrong file, but I've tried looking everywhere I've done changes before this error appeared.
This is my twig code:
{% extends 'base.html.twig' %}
{% block body %}
<div class="container">
{% form_theme form 'bootstrap_4_layout.html.twig' %}
{{ form_start(form) }}
<br>
Name {{ form_widget(form.name) }}
Price {{ form_widget(form.price) }}
Available {{ form_widget(form.available) }}
Date {{ form_widget(form.date) }}
<div class="row js-ticket-time-wrapper"
data-prototype="{{ form_widget(form.times.vars.prototype)|e('html_attr') }}"
data-index="{{ form.times|length }}">
{% for time in form.times %}
<div class="col-xs-4 js-ticket-time-item">
<a href="#" class="js-remove-time pull-right">
<span class="fa fa-close"></span>
</a>
{{ form_row(time.name) }}
</div>
{% endfor %}
<a href="#" class="js-ticket-time-add">
<span class="fa fa-plus-circle"></span>
Add another Time
</a>
</div>
<button type="submit" class="btn btn-primary" formnovalidate>Save</button>
{{ form_end(form) }}
</div>
{% endblock %}
{% block js %}
{{ parent() }}
<script>
jQuery(document).ready(function() {
var $wrapper = $('.js-ticket-time-wrapper');
$wrapper.on('click', '.js-remove-time', function(e) {
e.preventDefault();
$(this).closest('.js-ticket-time-item')
.fadeOut()
.remove();
});
$wrapper.on('click', '.js-ticket-time-add', function(e) {
e.preventDefault();
// Get the data-prototype explained earlier
var prototype = $wrapper.data('prototype');
// get the new index
var index = $wrapper.data('index');
// Replace '__name__' in the prototype's HTML to
// instead be a number based on how many items we have
var newForm = prototype.replace(/__name__/g, index);
// increase the index with one for the next item
$wrapper.data('index', index + 1);
// Display the form in the page before the "new" link
$(this).before(newForm);
});
});
</script>
{% endblock %}
I've also made changes in Entities Time & Ticket, but I don't think so this is connected somehow.
Here's my TicketType Form:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name',TextType::class)
->add('price',MoneyType::class)
->add('available',IntegerType::class)
->add('date',DateType::class)
->add('times', CollectionType::class, array(
'entry_type' => \App\Form\TimeType::class,
'allow_delete' => true,
'by_reference' => false,
'allow_add' => true,
));
}
And this is TimeType:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name', TextType::class);
}
Because the classname of your formtype TimeType is the same as the Symfony TimeType (https://symfony.com/doc/current/reference/forms/types/time.html) the formtheme layout tries to render the symfony type instead of yours. You can see the symfony TimeType has an option called widget so the formtype is expecting this type.
So try to rename your TimeType to something else like TicketTimeType.
Or you can rename your block prefix like this in your TimeType:
public function getBlockPrefix()
{
return "ticket_time_type";
}

Printing all errors in one go within twig

I want to print all errors in one go for all the fields however {{ form_errors(form) }} doesn't print anything so because of that I have to use if not form.vars.valid statement to print errors however all the individual error messages are being wrapped within <ul><li>Message</li></ul> which is annoying. I know that 'error_bubbling' => true solves the issue but creates another issue which is making field borders red.
How can I solve this issue? I simply want to print error without tags.
Note: I can use {{ form_errors(form.name)|striptags }} but it adds overheads cos my form is massive.
FORM TYPE
class BrandsType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->setAction($options['action'])
->setMethod('POST')
->add('name', 'text', array('label' => 'Name'))
->add('button', 'submit', array('label' => 'Submit'))
;
}
}
TWIG
{% extends '::base.html.twig' %}
{% block body %}
{{ form_start(form, {attr: {novalidate:'novalidate'}}) }}
{% if not form.vars.valid %}
<div class="global_form_errors">
{{ form_errors(form.name) }}
{{ form_errors(form.origin) }}
</div><br />
{% endif %}
<div>
{{ form_label(form.name) }}
{% if form.name.vars.errors|length != '' %}
{{ form_widget(form.name, { attr: {'class': 'field_red_border'} }) }}
{% else %}
{{ form_widget(form.name) }}
{% endif %}
</div>
<div>
{{ form_widget(form.button) }}
</div>
{{ form_end(form) }}
{% endblock %}
Customize the field template
You can apply a form template to your form, check this documentation at Form Theming chapter.
If you prefer theming the field directly in your template, check the gray box in this documentation at the Global Form Theming chapter.
Check this documentation for more form customization.
Pass the errors to the template
When you submit the form, your code will call this method $form->handleRequest($request); which will check if the user input properly the data.
Based on the documentation of the Form class you can get a list of errors by calling $form->getErrors(). The variable $form is an instance of the Form class and not the FormView class. You may want to enable error bubbling.
Now that you know how to get the errors you can pass them as a variable to your template.

Symfony twig how to add class to a form row

I am building a project in Symfony 2.3 using Twig. I want to add a class to the form row block. I am using a form theme file which contains:
{% block form_row %}
<div class="form-row">
{{ form_label(form) }}
{{ form_widget(form) }}
{{ form_errors(form) }}
</div>
{% endblock %}
Now some of my form rows I want to add an extra class form-row-split. I can't figure out how to do this properly. The way I have it almost-working is:
{% block form_row %}
{% set attr = attr|merge({'class': 'form-row' ~ (attr.class is defined ? ' ' ~ attr.class : '') ~ (errors|length > 0 ? ' error' : '')} ) %}
<div {{ block('widget_container_attributes') }}>
{{ form_label(form) }}
{{ form_widget(form) }}
{{ form_errors(form) }}
</div>
{% endblock %}
(Note, I've left the error class logic in there too as that needs to stay).
Then in the form builder:
$builder
->add('first_name', 'text', array(
'attr' => array(
'class' => 'form-row-split'
)
));
This almost works but it adds this class everywhere and also adds the widget id to the row!
<div id="myform_first_name" class="form-row form-row-split">
<label for="myform_first_name">First name</label>
<input id="myform_first_name" class="form-row-split" type="text" name="myform[first_name]">
</div>
I can think of a few potential solutions but none of them are pretty or straight forward. Surely there must be a simple way of doing this?
There is a fairly simple solution to this problem actually. I just needed a form type extension to extend the base form type to allow an extra available option: http://symfony.com/doc/2.3/cookbook/form/create_form_type_extension.html
Following through the example in the docs, I created a new form type extension:
// src/Acme/FrontendBundle/Form/Extension/FormTypeExtension.php
namespace Acme\FrontendBundle\Form\Extension;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
/**
* Class FormTypeExtension
* #package Acme\FrontendBundle\Form\Extension
*/
class FormTypeExtension extends AbstractTypeExtension
{
/**
* Extends the form type which all other types extend
*
* #return string The name of the type being extended
*/
public function getExtendedType()
{
return 'form';
}
/**
* Add the extra row_attr option
*
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'row_attr' => array()
));
}
/**
* Pass the set row_attr options to the view
*
* #param FormView $view
* #param FormInterface $form
* #param array $options
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['row_attr'] = $options['row_attr'];
}
}
Then I registered the service in my bundle...
<!-- Form row attributes form extension -->
<service id="acme.form_type_extension" class="Acme\FrontendBundle\Form\Extension\FormTypeExtension">
<tag name="form.type_extension" alias="form" />
</service>
Since every widget extends the base form type this then allows me to pass this new row_attr option through on any field, eg:
$builder
->add('first_name', 'text', array(
'row_attr' => array(
'class' => 'form-row-split'
)
));
Then the twig overrides to make use of the new row_attr option:
{% block form_row %}
<div {{ block('form_row_attributes') }}>
{{ form_label(form) }}
{{ form_widget(form) }}
{{ form_errors(form) }}
</div>
{% endblock form_row %}
{% block form_row_attributes %}
{% spaceless %}
{% for attrname, attrvalue in row_attr %}{{ attrname }}="{{ attrvalue }}" {% endfor %}
{% endspaceless %}
{% endblock form_row_attributes %}
And it's done!
(For completeness, my full twig override still merges in the form-row and error classes in like so:
{% set row_attr = row_attr|merge({'class': 'form-row' ~ (row_attr.class is defined ? ' ' ~ row_attr.class : '') ~ (errors|length > 0 ? ' error' : '')} ) %}
.. but thats not really necessary for answering my own question :P )
Docs say: you always able to pass attr to rendered element:
{{ form_start(form, {'attr': {'class': 'your-class'}} ) }}
{{ form_label(form, {'attr': {'class': 'your-class'}}) }}
{{ form_widget(form, {'attr': {'class': 'your-class'}}) }}
{{ form_errors(form, {'attr': {'class': 'your-class'}}) }}
{{ form_end(form) }}
Below is a clone of answer by #lopsided but with changes reflecting latest Symfony structure changes (v. 2.7+):
There is a fairly simple solution to this problem actually. I just needed a form type extension to extend the base form type to allow an extra available option: http://symfony.com/doc/master/form/create_form_type_extension.html
Following through the example in the docs, I created a new form type extension:
// src/Acme/FrontendBundle/Form/Extension/FormTypeExtension.php
namespace Acme\FrontendBundle\Form\Extension;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Class FormTypeExtension
* #package Acme\FrontendBundle\Form\Extension
*/
class FormTypeExtension extends AbstractTypeExtension
{
/**
* Extends the form type which all other types extend
*
* #return string The name of the type being extended
*/
public function getExtendedType()
{
return FormType::class;
}
/**
* Add the extra row_attr option
*
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'row_attr' => []
));
}
/**
* Pass the set row_attr options to the view
*
* #param FormView $view
* #param FormInterface $form
* #param array $options
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['row_attr'] = $options['row_attr'];
}
}
Then I registered the service in my bundle...
<!-- Form row attributes form extension -->
<service id="acme.form_type_extension" class="Acme\FrontendBundle\Form\Extension\FormTypeExtension">
<tag name="form.type_extension" alias="form" extended_type="Symfony\Component\Form\Extension\Core\Type\FormType" />
</service>
Since every widget extends the base form type this then allows me to pass this new row_attr option through on any field, eg:
$builder
->add('first_name', TextType:class, [
'row_attr' => [
'class' => 'form-row-split'
]
]);
Then the twig overrides to make use of the new row_attr option:
{% block form_row %}
<div {{ block('form_row_attributes') }}>
{{ form_label(form) }}
{{ form_widget(form) }}
{{ form_errors(form) }}
</div>
{% endblock form_row %}
{% block form_row_attributes %}
{% spaceless %}
{% for attrname, attrvalue in row_attr %}{{ attrname }}="{{ attrvalue }}" {% endfor %}
{% endspaceless %}
{% endblock form_row_attributes %}
And it's done!
(For completeness, my full twig override still merges in the form-row and error classes in like so:
{% set row_attr = row_attr|merge({'class': 'form-row' ~ (row_attr.class is defined ? ' ' ~ row_attr.class : '') ~ (errors|length > 0 ? ' error' : '')} ) %}
.. but thats not really necessary for answering my own question :P )
What I did was simpler (though maybe a bit less clean?).
Pass the class for the form row via a field's "data" attribute :
// template.html.twig
{{ form_start(form) }}
{{ form_row(form.field, {'attr': {'data-row-class': 'my-row-class'} }) }}
{{ form_end(form) }}
And then handle it in the form theme template this way :
// form-theme.html.twig
{% block form_row -%}
{% set row_class = attr['data-row-class'] | default('') %}
<div class="{{ row_class }}">
{{- form_label(form) -}}
{{- form_widget(form) -}}
{{- form_errors(form) -}}
</div>
{%- endblock form_row %}
Which gives this :
<form name="formName" method="post">
<div class="my-row-class">
<label for="formName_field">Field label</label>
<input type="text" id="formName_field" name="formName[field]" data-row-class="my-row-class">
</div>
</form>

Symfony: How to avoid custom form-types getting wrapped in a div automatically?

UserType Form:
class UserType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('email', 'email', ['label' => 'EMail']);
// various other fields....
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'validation_groups' => array('registration'),
'data_class' => 'Vendor\Model\Entity\User',
));
}
public function getName()
{
return 'form_user';
}
}
TutorType Form:
class TutorType extends Translate
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('user', new UserType(), ['label' => false]);
$builder->add('school', 'entity', [
'class' => 'Model:School',
'property' => 'name',
'label' => 'Label'
]);
// Various other fields
$builder->add('save', 'Submit');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
//'validation_groups' => array('registration'),
'data_class' => 'Vendor\Model\Entity\Tutor',
'cascade_validation' => true,
));
}
public function getName()
{
return 'form_tutor';
}
}
When rendering, the UserType is rendered inside a div, i cant find a way to overcome this.
The Form is rendered as
<form name="form_tutor"
method="post"
action=""
novalidate="novalidate"
class="form-horizontal form-horizontal"
id="form_tutor">
<div id="form_tutor"
novalidate="novalidate"
class="form-horizontal">
<div class="form-group">
<div class="col-lg-10">
<div id="form_tutor_user">
<div class="form-group">
<label class="col-lg-2 control-label aaaa required"
for="form_tutor_user_email">EMail</label>
<div class="col-lg-10">
<input type="email"
id="form_tutor_user_email"
name="form_tutor[user][email]"
required="required"
class="form-control" />
</div>
</div>
</div>
</div>
</div>
<div class="form-group">
<label class="col-lg-2 control-label aaaa required"
for="form_tutor_tutorType">Type</label>
<div class="col-lg-10">
<select id="form_tutor_tutorType"
name="form_tutor[tutorType]"
class="form-control">
</select>
</div>
</div>
<div class="form-group">
<div class="col-lg-offset-2 col-lg-10">
<button type="submit"
id="form_tutor_save"
name="form_tutor[save]"
class="btn btn-default">Speichern</button>
</div>
</div><input type="hidden"
id="form_tutor__token"
name="form_tutor[_token]"
class="form-control"
value="s6i6zPxJs7KU5CiEe8i6Ahg_ca8rc2t5CnSk5yAsUhk" />
</div>
</form>
The form_tutor_user is wrapped in a own form-group div.
I tried to overwrite the form_tutor_user_widget but this is one level to deep. (And only a quick fix, it should be globally applied to all form type - Classes)
How can i change the theme so all custom types are not wrapped with the default form_row template?
Or how do i know in twig when a "subform" is rendered?
so i can decide to print the <div class="form-group"> when the child-node is not a subform, or skip it, if this is the case.
TIA
By default, in the base form theme:
{% block form_row %}
{% spaceless %}
<div>
{{ form_label(form) }}
{{ form_errors(form) }}
{{ form_widget(form) }}
</div>
{% endspaceless %}
{% endblock form_row %}
And, for custom compound forms:
{% block form_widget_compound %}
{% spaceless %}
<div {{ block('widget_container_attributes') }}>
{% if form.parent is empty %}
{{ form_errors(form) }}
{% endif %}
{{ block('form_rows') }}
{{ form_rest(form) }}
</div>
{% endspaceless %}
{% endblock form_widget_compound %}
Unless you changed something here, the DIV you see should come from either one or the other bit of template.
However, in your specfic example, if form_tutor_user_row is defined, the first bit is never used, and if form_tutor_user_widget is defined, the last bit is never used.
Back to your question. Your question is :
"How can i change the theme so all custom types are not wrapped with the default form_row template?"
Here is the problem the way I see it: you want that your TOP forms (the form in which all sub-forms are included) all have a common way of rendering, in sections. Each section will be included in a DIV with class="form-group". You may want to throw in some additional rendering operations but I will limit myself to this to keep things simple.
What you need to do then is to create a specfic form type and make all your TOP forms inherit from this new form type. For instance:
class TopType extends AbstractType
{
public function getName()
{
return 'top_form';
}
}
... and an inherited form:
class MyFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
...
}
public function getName()
{
return 'my_form';
}
public function getParent()
{
return 'top_form';
}
}
As you can see, there is no need to make PHP inheritance for form theming inheritance to work.
Template-theming-wise (can I even say that?), if no specific form theming is set for my_form, Symfony will understand that the default form theme to use here is the form theme of top_form, that you can define as such:
{% block top_form_widget %}
{% spaceless %}
{% for child in form %}
<div class="form-group">
{{ form_widget(child) }}
</div>
{% endfor %}
{{ form_rest(form) }}
{% endspaceless %}
{% endblock top_form_widget %}
I should add that this is a problem I already encountered and solved. Tell me how that works for you.
Edit:
To sum it up, what you have to do is:
Create the TopType form type,
Add the top_form_widget block in your form theme,
For all your root forms (i.e. top-level forms, forms that have no parent), add a getParent() method that will return the name of your TopType form ("top_form")
In theory, if you override the form_widget_compound block in a global form theme this way, it should work as you want:
// app/Resources/views/form.html.twig
{% block form_widget_compound %}
{% if form.parent is empty %}
<div {{ block('widget_container_attributes') }}>
{{ form_errors(form) }}
{% endif %}
{{ block('form_rows') }}
{{ form_rest(form) }}
{% if form.parent is empty %}
</div>
{% endif %}
{% endblock %}
And register your form theme:
// app/config/config.yml
twig:
form:
resources:
- "::form.html.twig"
I usually solve this problem by rendering the nested form's fields manually:
{{ form_row(form.tutor.school) }}
{{ form_row(form.tutor.user.email) }}
Probably that's not the most elegant solution, but it works for me and I haven't looked for an elegant one yet.
Bootstrap 3.3.4 I ended up doing this.
They key part of this:
<div class="{% if form.parent.parent is empty %}form-group{% endif %}{% if (not compound or force_error|default(false)) and not valid %} has-error{% endif %}">
Full template.
{% block form_row -%}
<div class="{% if form.parent.parent is empty %}form-group{% endif %}{% if (not compound or force_error|default(false)) and not valid %} has-error{% endif %}">
{% if form.parent.parent is null and label is not empty %}
{{- form_label(form) -}}
{% elseif label is empty %}
{{- form_label(form) -}}
{% endif %}
{% if compound is empty %}<div class="{{ block('form_group_class') }}">{% endif %}
{{- form_widget(form) -}}
{{- form_errors(form) -}}
{% if compound is empty %}</div>{% endif %}
</div>
{%- endblock form_row %}
Maybe it is not an elegant solution, however works for me, because I was also trying to find the solution.
As an example:
{% for custom_field in form.custom_fields %}
<div class="edit_custom">
{{ form_row(custom_field.name) }}
{{ form_row(custom_field.value) }}
</div>
{% endfor %}
<script>
$('.edit_custom').find('input').unwrap();
</script>
Try using form_themes.
First, in you parent template define the form theme:
{% form_theme form with ['BundleName:ControlerName:somesubform_form.html.twig'] %}
btw replace BundleName, ControllerName and somesubform with the proper names.
then render it with:
{{ form_row(form.somesubform) }}

Link in form label using route

In my registration form i have a checkbox "I accept the terms", and want to link the word "terms" to my terms page.
Is there a way to add a link to a form label, using a route? (preferably without injecting the container in the form)
As the solution above somehow didn't work for me I solved it using the solution suggested here: https://gist.github.com/marijn/4137467
OK, so here i how I did it:
{% set terms_link %}<a title="{% trans %}Read the General Terms and Conditions{% endtrans %}" href="{{ path('get_general_terms_and_conditions') }}">{% trans %}General Terms and Conditions{% endtrans %}</a>{% endset %}
{% set general_terms_and_conditions %}{{ 'I have read and accept the %general_terms_and_conditions%.'|trans({ '%general_terms_and_conditions%': terms_link })|raw }}{% endset %}
<div>
{{ form_errors(form.acceptGeneralTermsAndConditions) }}
{{ form_widget(form.acceptGeneralTermsAndConditions) }}
<label for="{{ form.acceptGeneralTermsAndConditions.vars.id }}">{{ general_terms_and_conditions|raw }}</label>
</div>
The best way is to overwrite the twig block used to render that specific label.
First, check the form fragment naming section of the docs. Then create a new block in your form template with the the appropriate name. Don't forget to tell twig to use it:
{% form_theme form _self %}
For the next step check the default form_label block.
You'll probably only need a portion of it, something like this (I'm leaving the default block name here):
{% block form_label %}
{% spaceless %}
<label{% for attrname, attrvalue in label_attr %} {{ attrname }}="{{ attrvalue }}"{% endfor %}>
{{ label|trans({}, translation_domain) }}
</label>
{% endspaceless %}
{% endblock %}
As an option, you can do so:
->add('approve', CheckboxType::class, [
'label' => 'Text part without link',
'help' => 'And download it',
'help_html' => true,
])
In Symfony 5.1 there are new form improvements.
HTML contents are allowed in form labels!
HTML contents are escaped by default in form labels for security reasons. The new label_html boolean option allows a form field to include HTML contents in their labels, which is useful to display icons inside buttons, links and some formatting in checkbox/radiobutton labels, etc.
// src/Form/Type/TaskType.php
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// ...
->add('save', SubmitType::class, [
'label' => ' Save',
'label_html' => true,
])
;
}
}
In your case you can set form label directly from template and pass the route there.
{{ form_widget(form.acceptTermsAndConditions, {
label: '' ~ "I accept ..."|trans ~ '',
label_html: true
})
}}
My solution was another:
form:
$builder
->add(
'agree_to_rules',
'checkbox',
[
'required' => true,
'label' => 'i_agree_to'
]
);
And html:
<span style="display:inline-block">
{{ form_widget(form.agree_to_rules) }}
</span>
<span style="display:inline-block">
rules
</span>
And looks the same :)
A very simple way to do it would be
{{ form_widget(form.terms, { 'label': 'I accept the terms and conditions' }) }}
You can also do this if you want to use translation
In your translation file for example messages.en.yml add
terms:
url: 'I accept the terms and conditions'
And in your view add
{{ form_widget(form.terms, { 'label': 'terms.url'|trans({'%url%': path('route_to_terms')}) }) }}

Resources