How to detect widget type in Symfony Twig rendering? - symfony

For rendering form rows, I need to detect what type of widget is being rendered. For example for checkboxes I want the label before the input, so I did this:
{% block form_row %}
{% spaceless %}
<div class="row">
{% if form.vars.block_prefixes[1] == "checkbox" %}
{{ form_widget(form) }}
{{ form_label(form) }}
{{ form_errors(form) }}
{% else %}
<div class="small-12 medium-3 columns">
{{ form_label(form) }}
</div>
{{ form_widget(form) }}
{{ form_errors(form) }}
{% endif %}
</div>
{% endspaceless %}
{% endblock form_row %}
I used form.vars.block_prefixes[1] to determine the widget it's about to render. Is that right? Or is there a better method? I can't seem to find it in the docs.

If you see symfony cookbook, you can find:
the block name is a combination of the field type and which part of the field is being rendered (e.g. widget, label, errors, row)
So, to customize checkbox form type rendering, you can define checkbox_widget block and checkbox_row for widgets order:
{% block checkbox_widget %}
{% spaceless %}
<label {% for attrname, attrvalue in label_attr %} {{ attrname }}="{{ attrvalue }}"{% endfor %}>
{{ label }}
</label>
<input type="checkbox" {{ block('widget_attributes') }}{% if value is defined %} value="{{ value }}"{% endif %}{% if checked %} checked="checked"{% endif %} />
{% endspaceless %}
{% endblock checkbox_widget %}
{% block checkbox_row %}
{% spaceless %}
{{ form_widget(form) }}
{{ form_errors(form) }}
{% endspaceless %}
{% endblock checkbox_row %}

If there is any data you need in the templates and symfony doesn't provide it by default you can always create a form type extension
A form type extension is a mechanism that let's you extend many form types in a single place. You can extend all form types provided you define the right extended type (FormType::class if you want to extend all forms).
This extension provides both the block_type for the current instance and the class name for all form types. This let's you use these variables in any generic template (such as widget_attributes).
I only tested it on symfony 3.4
<?php
namespace YourBundle\Form\Extension;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\FormType;
class FormTypeExtension extends AbstractTypeExtension
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
$type = $form->getConfig()->getType();
$inner_type = $type->getInnerType();
$view->vars = array_replace($view->vars, array(
'block_prefix' => $type->getBlockPrefix(),
'type_name' => get_class($inner_type)
));
}
public function getExtendedType()
{
return FormType::class;
}
}

Related

Symfony form builder - how to put form title in h2 selector?

I want my form title to be displayed in h2 selector. I did something like that but it throws me an error "exception: "An exception has been thrown during the rendering of a template ("Notice: Undefined offset: -1").""
// how should I change THIS part? To change only the main form title/label?
// I made it work somehow but then it changes all labels... Is there some
// selector which allows to style MAIN title of the form?
{% block form_label %}
{% spaceless %}
<h2>{{ form_label(form) }}</h2>
{% endspaceless %}
{% endblock form_label %}
I've heard that I shouldn't put it in my project file in h2 selector just in form template. It's cleaner and even though documentation doesn't forbide that I was encouraged to do it another way and that's how I want to try it.
{% form_theme form 'Forms/base_form.html.twig' %}
{{ form_start(form) }}
{{ form_label(form, 'Project title', { 'label_attr': {'class': 'main-form-label'} }) }}
// so as I shouldn't put all that line in <h2> can I somehow do it in template between {% block form_label %} ?
{{ form_row(form.title, {'label': 'My title'}) }}
{{ form_row(form.isComplete, {'label': 'Dropdown'}) }}
{{ form_row(form.comment, {'label': 'Comment'}) }}
{{ form_row(form.submit, {'label': 'Submit'}) }}
{{ form_end(form) }}
Also... Whats the difference/what should I use - {% block form_label %} or {%- block form_label -%}
My whole template:
{% block form_label %}
{% spaceless %}
<h2>{{ form_label(form) }}</h2>
{% endspaceless %}
{% endblock form_label %}
{% block form_row %}
{% spaceless %}
{{ form_widget(form) }}
{{ form_errors(form) }}
{% endspaceless %}
{% endblock form_row %}
{% block submit_row %}
{% spaceless %}
<div class="col-12">
{{ form_widget(form) }}
</div>
{% endspaceless %}
{% endblock submit_row %}
{% block text_widget %}
{% spaceless %}
<div class="col-12">
<div>
{{ form_label(form) }}
</div>
{{ form_widget(form) }}
</div>
{% endspaceless %}
{% endblock text_widget %}
{% block choice_widget %}
{% spaceless %}
<span>
{{ form_label(form) }}
{% if expanded %}
{{ block('choice_widget_expanded') }}
{% else %}
{{ block('choice_widget_collapsed') }}
{% endif %}
{{ form_errors(form) }}
</span>
{% endspaceless %}
{% endblock choice_widget %}
In case you get stuck somewhere ;)
We use to do it this way (when we want custom rendering).
First, we extend the FormType :
namespace App\Form\Extension;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
class FormTypeExtension extends AbstractTypeExtension
{
public function getExtendedType()
{
return FormType::class;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('main_title', false);
$resolver->setAllowedTypes('main_title', 'boolean');
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['main_title'] = $options['main_title'];
}
}
Think to register you extension in Symfony !
Then you can use it in your form builder this way :
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
'myField',
TextType::class,
array(
'main_title' => true
));
}
Finally, in your template :
{% block form_row -%}
{% spaceless %}
{% if main_title %}
<h2>{{ form_label(form) }}</h2>
{% else %}
{{ form_label(form) }}
{{ form_widget(form) }}
{% endif %}
{% endspaceless %}
{%- endblock form_row %}
Feel free to override that block with your needs ;-)

Custom form field template with twig

I'd like to create a custom template in twig to render a form field.
Example:
{{ form_row(form.field) }}
This can be overriden by form theming
{% block form_row %}
... custom code
{% endblock form_row %}
What I would like to do is this:
{% block custom_row %}
... custom code
{% endblock custom_row %}
and use it like this:
{{ custom_row(form.field }}
however, this throws an exception that method custom_row is not found.
My understanding is that this can be done with Twig extension, but I don't know how to register a block to be a function.
Update
what I actually want:
I use twitter bootstrap and a bundle which overrides all the form themes. And it renders a div around a radio, so it can't be inlined. So I wanted to do something like this:
copy their template and get rid of the div:
{% block inline_radio_row %}
{% spaceless %}
{% set col_size = col_size|default(bootstrap_get_col_size()) %}
{% if attr.label_col is defined and attr.label_col is not empty %}
{% set label_col = attr.label_col %}
{% endif %}
{% if attr.widget_col is defined and attr.widget_col is not empty %}
{% set widget_col = attr.widget_col %}
{% endif %}
{% if attr.col_size is defined and attr.col_size is not empty %}
{% set col_size = attr.col_size %}
{% endif %}
{% if label is not sameas(false) %}
{% if not compound %}
{% set label_attr = label_attr|merge({'for': id}) %}
{% endif %}
{% if required %}
{% set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ ' required')|trim}) %}
{% endif %}
{% if label is empty %}
{% set label = name|humanize %}
{% endif %}
{% set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ ' radio-inline')|trim}) %}
<label{% for attrname, attrvalue in label_attr %} {{ attrname }}="{{ attrvalue }}"{% endfor %}>
{{ block('radio_widget') }}
{{ label|trans({}, translation_domain) }}
</label>
{% else %}
{{ block('radio_widget') }}
{% endif %}
{{ form_errors(form) }}
{% endspaceless %}
{% endblock inline_radio_row %}
and then
{{ inline_radio_row(form.field) }}
I ended up just overriding the whole theme, and added ifs around the div in question, a the class (radio-inline). But I'm still wondering if there's a way to make this work. Seems like it makes you work so hard for something so simple.
Update 2
I found the functionality:
class FormExtension extends \Twig_Extension
{
public function getFunctions()
{
return array(
'inline_radio_row' => new \Twig_Function_Node(
'Symfony\Bridge\Twig\Node\SearchAndRenderBlockNode',
array('is_safe' => array('html'))
),
);
}
}
This does exactly what I want, but it says it's deprecated. Anyone knows an updated version of how to use this?
Update 3
Similar functionality can be also achieved with http://twig.sensiolabs.org/doc/tags/include.html
You can use the twig functions for each part of a form row:
form_label(form.field)
form_widget(form.field)
form_errors(form.field)
For example:
<div class="form_row">
{{ form_label(form.field) }} {# the name of the field #}
{{ form_errors(form.field) }} {# the field #}
{{ form_widget(form.field) }} {# the errors associated to the field #}
</div>
You can use form theming.
Step by step:
1. Form Type Class
Check the name in your class
public function getName() {
return 'hrQuestionResponse';
}
2. Include a custom theme in your template
{% form_theme form 'InterlatedCamsBundle:Form:fields.html.twig' %}
3. Find the block
Can be quite difficult. For the bootstrap bundle, as you seem to have found it is in ./vendor/braincrafted/bootstrap-bundle/Braincrafted/Bundle/BootstrapBundle/Resources/views/Form/bootstrap.html.twig and you have found the block radio_row. I have been finding the block by putting output in the source template and overriding more blocks than I need. In 2.7 there is a theme 'rendering call graph'.
4. Override the block
Copy the block from the master template and call it replace the standard term with the name of in the FormType found in step 1.
Don't forget to also change the endblock name.
e.g.
{% block hrQuestionResponse_widget %}
hrQuestionResponse_row
{% spaceless %}
{% set class = '' %}
...
{% endspaceless %}
{% endblock hrQuestionResponse_widget %}
In your case because you can only call form_widget() you will need to override _widget. You could extract only the content that you need, or you can override the block chain to radio_row.

Update from 2.0 to 2.1: Class for <label> gets lost on it's way

I just updated symfony2 from 2.0.16 to 2.1.2 and now I've got the problem, that my class for the label isn't added anymore.
In the Twig-template I include the field like this:
<div class="row{% if form_errors(form.object) %} _error{% endif %}">
{{ form_label(form.object, null, { 'attr': {'class': '_hint'} }) }}
{{ form_widget(form.object, { 'attr': {'class': 'c6'} }) }}
</div>
And my extension of the default form layout looks like this:
{% use 'form_div_layout.html.twig' with field_label as base_field_label %}
{% block field_label %}
{{ block('base_field_label') }}
{% if attr.class is defined and '_hint' == attr.class %}
<div>
some
</div>
{% endif %}
{% endblock %}
The strange thing is, that the attr.class value is set, as the <div> gets rendered. But the class is not added to the <label> anymore.
you should overwrite a form_label block instead of field_label because field_label is deprecated
You should use a label_attr array instead of attr, ie:
{% if label_attr.class is defined and '_hint' == label_attr.class %}

Symfony2 and form theming/customization (required/help/errors)

Maybe I'm overlooking something, and hopefully this is done very easy.
I have a form and what I want in the end is the following result:
Fields which:
are mandatory/required
have an error currently
have help
should get an extra a-Tag after the label and an extra div, filled with the help and/or the error, if applicable.
What I got to work is, that required fields get the a-Tag by using this:
{% use 'form_div_layout.html.twig' with field_label as base_field_label %}
{% block field_label %}
{{ block('base_field_label') }}
{% if required %}
<span> </span>
{% endif %}
{% endblock %}
So, what I tried already were different versions of this:
{% use 'form_div_layout.html.twig' with field_label as base_field_label %}
{% block field_label %}
{{ block('base_field_label') }}
{% if required or help is defined %}
<span> </span>
{% endif %}
{% endblock %}
{% block field_row %}
{% spaceless %}
<div class="row">
{% if required or help is defined %}
<div>
{{ form_errors(form) }}
{{ help }}
</div>
{% endif %}
{{ form_label(form) }}
{{ form_widget(form, { 'attr': {'class': 'grid_4'} }) }}
</div>
{% endspaceless %}
{% endblock field_row %}
And I can't get this to work.
So my questions are:
Where do I get the help text from, which can also contain HTML? I tried this within the form builder without success - but at least with an exception:
$builder ->add('subject', 'text', array(
'label' => 'Subject',
'help' => 'Can be formatted content with <strong>HTML-Elements</strong>',
));
How can I tell that the current field has an error (to add a class to the row) and if so also display it? {{ form_errors(form) }} did not output anything, no matter where I place it within `field_row˚.
There is no help text, you have to create Form Extension for field and add it to default options.
Example in SF 2.1 Beta 1:
namespace Webility\Bundle\WebilityBundle\Form\Extension;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormViewInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class HelpFormTypeExtension extends AbstractTypeExtension
{
public function buildView(FormViewInterface $view, FormInterface $form, array $options){
$view->setVar('help', $options['help']);
}
public function getExtendedType(){
return 'field';
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array(
'help' => null
));
}
}
And register it as a service:
<service id="webility.form.extension.help" class="Webility\Bundle\WebilityBundle\Form\Extension\HelpFormTypeExtension">
<tag name="form.type_extension" alias="field" />
</service>
For the errors question:
Do you have any errors to print? Check that in controller if validation fails:
echo '<pre>'; print_r( $form->getErrorsAsString() ); echo '</pre>'; exit;
To solve it as stated in my question Maciej Pyszyński's anwser was very helpful.
I solved it in this case in another way, which I also want to post here. According to the manual "Adding "help" messages" I build this:
Note This solution won't work together with the formbuilder and needs some tweaking in twig.
To get the help ''-tags (actually they are divs now) …
{% block field_label %}
{{ block('base_field_label') }}
{% if attr.class is defined and '_hint' == attr.class %}
<div>
<a><span class="help">Help Icon</span></a>
<div class="tooltip">
{% if help is defined %}
{{ help|raw }}
{% else %}
Somebody forgot to insert the help message
{% endif %}
</div>
</div>
{% endif %}
{% endblock %}
To get the right class on an error
{% block field_row %}
{% spaceless %}
<div class="row{% if form_errors(form) %} error{% endif %}">
{{ form_label(form) }}
{{ form_widget(form, { 'attr': {'class': 'grid_4'} }) }}
</div>
{% endspaceless %}
{% endblock field_row %}
And the call from the template
<div class="row{% if form_errors(form.url) %} _error{% endif %}">
{{ form_label(form.field, null, { 'attr': {'class': '_hint'}, 'help': 'Help text or variable containing it' }) }}
{{ form_widget(form.field, { 'attr': {'class': 'grid_4'} }) }}
</div>

How to add a css class on a "form_row" in twig template

I would like to know how I can add a css class on a "{{ form_row() }}" in twig. For the moment, I have this code :
{{ form_row(form.username, {'label' : "Login", 'attr': {'class': 'loginForm'}}) }}
... But the CSS class "loginForm" isn't used in the HTML code.
Thank you :) !
If you want the common class for the form_row (it means one class for form_label, form_widget and form_errors), you should customize a field_row block.
This article explains how to customize form fields: How to customize Form Rendering.
There are some methods to do this.
For example I'm using Method 2 (How to customize Form Rendering: Method 2):
{% extends 'form_div_layout.html.twig' %}
{% block field_row %}
{% spaceless %}
{% set class='' %}
{% if attr.class is defined %}
{% set class = 'class="' ~ attr.class ~ '"' %}
{% endif %}
<div {{ class }} >
{{ form_label(form, label|default(null)) }}
{{ form_errors(form) }}
{{ form_widget(form) }}
</div>
{% endspaceless %}
{% endblock field_row %}
My answer is very similar to nni6's but it allows you to pass through the entire attr array.
My HTML structure is for Twitter Bootstrap but you can have whatever you want. This example also places an error class on the wrapper div if there are any errors - this part is not required but is useful if you use Bootstrap:
{% extends 'form_div_layout.html.twig' %}
{% block field_row %}
{% spaceless %}
{% set field_row_classes = ['control-group'] %}
{% if errors|length > 0 %}
{% set field_row_classes = field_row_classes|merge(['error']) %}
{% endif %}
<div class="{{ field_row_classes|join(' ') }}">
{{ form_label(form, label|default(null)) }}
<div class="controls">
{{ form_widget(form, { 'attr' : attr|default({}) }) }}
{{ form_errors(form) }}
{% if help is defined %}
<p class="help-block">{{ help }}</p>
{% endif %}
</div>
</div>
{% endspaceless %}
{% endblock field_row %}
The difference here is that I'm calling form_widget with the attr array (if it was specified, defaults to {})
Now you can set up your form as normal but pass through a custom class:
{{ form_row(myentity.myproperty, { 'label' : 'mylabel', 'attr' : { 'class' : 'myclass' }}) }}
This post need an update !
Since Symfony 4.3, the row_attr option permits to add attributes (and so, some class). Let's check those links :
Here :
https://symfony.com/blog/new-in-symfony-4-3-more-form-improvements#row-attributes-in-form-themes
or more specifically : https://symfony.com/doc/current/reference/forms/types/form.html#row-attr
I use this code to configure bootstrap in symfony
{% block field_row %}
{% spaceless %}
<div class="control-group {% if errors|length > 0 %}error{% endif %}">
{{ form_label(form, label, { 'attr': { 'class': 'control-label' } }) }}
<div class="controls">
{{ form_widget(form, { 'attr' : attr|default({}) }) }}
{{ form_errors(form) }}
</div>
</div>
{% endspaceless %}
{% endblock field_row %}
The easiest way to accomplish this without overwriting blocks is this:
{% set attr = {'class' : form.myElement.vars.attr.class ~ ' addedClasses...', 'style' : form.myElement.vars.attr.style ~ '; addedStyles...'} %}
{{- form_row(form.myElement, {'attr' : attr}) -}}

Resources