Twig - Why can't I access a variable I've set? - symfony

For some reason, a variable I'm setting in one form template bloc is not available in a child form block.
I have an 'entity' field type to present a selection of checkboxes to allow the user to select related items...
$builder
->add( 'title' )
->add(
'apps',
'entity',
[
'class' => 'OurAdminBundle:App',
'choices' => $apps,
'property' => 'title',
'expanded' => true,
'multiple' => true
]
)
And here's the template that renders the form
// Effectively imported using the MopaBootstrapBundle
// {% form_theme form 'OurAdminBundle:Form:fields.html.twig %}
// Further in page theming
{% form_theme form _self %}
// Set variable when on the apps field, so it should be available to all child
// forms
{% block _gallery_apps_widget %}
{% set custom_checkboxes = 1 %}
{{ block('choice_widget') }}
{% endblock %}
// Attempt to retrieve the variable on the checkboxes within the apps entity
/ field
{% block checkbox_widget %}
{{ dump(custom_checkboxes|default(0) }} // Displays 0
{% endblock checkbox_widget %}
Here's the code from the fields.html.twig file (with minor debugging additions...
{% block choice_widget_expanded %}
{{ dump(custom_checkboxes|default(0)) }}
{% set custom_checkboxes = custom_checkboxes|default(0) %}
{{ dump(custom_checkboxes|default(0)) }}
{% spaceless %}
{% set label_attr = label_attr|merge({'class': (label_attr.class|default(''))}) %}
{% set label_attr = label_attr|merge({'class': (label_attr.class ~ ' ' ~ (widget_type != '' ? (multiple ? 'checkbox' : 'radio') ~ '-' ~ widget_type : ''))}) %}
{% if expanded %}
{% set attr = attr|merge({'class': attr.class|default(horizontal_input_wrapper_class)}) %}
{% endif %}
{% for child in form %}
{% if widget_type != 'inline' %}
<div class="{{ multiple ? 'checkbox' : 'radio' }}">
{% endif %}
<label{% for attrname, attrvalue in label_attr %} {{ attrname }}="{{ attrvalue }}"{% endfor %}>
{{ form_widget(child, {'horizontal_label_class': horizontal_label_class, 'horizontal_input_wrapper_class': horizontal_input_wrapper_class, 'attr': {'class': attr.widget_class|default('')}}) }}
{{ child.vars.label|trans({}, translation_domain) }}
</label>
{% if widget_type != 'inline' %}
</div>
{% endif %}
{% endfor %}
{% endspaceless %}
{% endblock choice_widget_expanded %}
... which successfully displays '1' on both counts.
I've racked my brains over this one, but can't for the life of me understand why I can't access the variable in the checkbox_widget block. Please help.

This is due to how Symfony renders form fields when calling form_widget() or any other form* family of functions.
Symfony creates a new separate scope which do not share the scope of the parent (in order to prevent scope polluting while rendering fields).
If you which to pass a variable to the checkbox widget, edit the form_widget call in the choice_widget_expanded to pass on the custom_checkboxes as so (added tabbing for clarity only):
{{ form_widget(child, {
'horizontal_label_class': horizontal_label_class,
'horizontal_input_wrapper_class': horizontal_input_wrapper_class,
'attr': {'class': attr.widget_class|default('')},
'custom_checkboxes': custom_checkboxes
}) }}

Related

KnpMenuBundle add icon/glyphicon

I have tried to set an icon next to the link of the menu generated by knpMenuBundle but no way to get it.
I found some solutions like ->setAttribute or ->setExtra, but I can't make it work.
Do I have to add specific lines to the base template of knp ?
Do something special in the twig render ?
Here is what I have tried so far:
//Builder.php
$public function mainMenu(FactoryInterface $factory, array $options)
{
$menu = $factory->createItem('root');
$menu->setChildrenAttribute('class', 'nav navbar-nav ');
$menu->addChild('Mes Informations',['route' => 'espace_client_mesInformations'])
->setAttribute('icon','fa fa-home');
$menu->addChild('Mes Factures', array('route' => 'espace_client_mesFactures'));
$menu->addChild('Mon Extrait de Compte', array('route' => 'espace_client_mesReglements'));
$menu->addChild('Mes Services', array('route' => 'espace_client_mesServices'));
$menu->addChild('Mes Consommations', array('route' => 'espace_client_mesConsommations'));
$menu['Mes Consommations']->addChild('Télephone', array('route' => 'espace_client_mesConsoTelephone'));
$menu['Mes Consommations']->addChild('Internet', array('route' => 'espace_client_mesConsoInternet'));
$menu->addChild('Mes Liens Directs', array('route' => 'espace_client_mesLiens'));
return $menu;
}
//menu.html.twig
<div class="panel panel-default">
<div class="panel-heading">
<center><h4> Menu </h4></center>
</div>
<div class="panel-body">
{{ knp_menu_render('EspaceClientBundle:Builder:mainMenu',{'currentAsLink':false, 'template': 'EspaceClientBundle:Menu:knp_menu.html.twig'}) }}
</div>
If anyone have an answer ?
Thanks.
Edit:
I'm a little bit confused by the different files of the bundle, I must have a problem in my php code so there is the content of them:
//knp_menu.html.twig:
{% extends 'knp_menu_base.html.twig' %}
{% macro attributes(attributes) %}
{% for name, value in attributes %}
{%- if value is not none and value is not same as(false) -%}
{{- ' %s="%s"'|format(name, value is same as(true) ? name|e : value|e)|raw -}}
{%- endif -%}
{%- endfor -%}
{% endmacro %}
{% block compressed_root %}
{% spaceless %}
{{ block('root') }}
{% endspaceless %}
{% endblock %}
{% block root %}
{% set listAttributes = item.childrenAttributes %}
{{ block('list') -}}
{% endblock %}
{% block list %}
{% if item.hasChildren and options.depth is not same as(0) and item.displayChildren %}
{% import _self as knp_menu %}
<ul{{ knp_menu.attributes(listAttributes) }}>
{{ block('children') }}
</ul>
{% endif %}
{% endblock %}
{% block children %}
{# save current variables #}
{% set currentOptions = options %}
{% set currentItem = item %}
{# update the depth for children #}
{% if options.depth is not none %}
{% set options = options|merge({'depth': currentOptions.depth - 1}) %}
{% endif %}
{# update the matchingDepth for children #}
{% if options.matchingDepth is not none and options.matchingDepth > 0 %}
{% set options = options|merge({'matchingDepth': currentOptions.matchingDepth - 1}) %}
{% endif %}
{% for item in currentItem.children %}
{{ block('item') }}
{% endfor %}
{# restore current variables #}
{% set item = currentItem %}
{% set options = currentOptions %}
{% endblock %}
{% block item %}
{% if item.displayed %}
{# building the class of the item #}
{%- set classes = item.attribute('class') is not empty ? [item.attribute('class')] : [] %}
{%- if matcher.isCurrent(item) %}
{%- set classes = classes|merge([options.currentClass]) %}
{%- elseif matcher.isAncestor(item, options.matchingDepth) %}
{%- set classes = classes|merge([options.ancestorClass]) %}
{%- endif %}
{%- if item.actsLikeFirst %}
{%- set classes = classes|merge([options.firstClass]) %}
{%- endif %}
{%- if item.actsLikeLast %}
{%- set classes = classes|merge([options.lastClass]) %}
{%- endif %}
{# Mark item as "leaf" (no children) or as "branch" (has children that are displayed) #}
{% if item.hasChildren and options.depth is not same as(0) %}
{% if options.branch_class is not empty and item.displayChildren %}
{%- set classes = classes|merge([options.branch_class]) %}
{% endif %}
{% elseif options.leaf_class is not empty %}
{%- set classes = classes|merge([options.leaf_class]) %}
{%- endif %}
{%- set attributes = item.attributes %}
{%- if classes is not empty %}
{%- set attributes = attributes|merge({'class': classes|join(' ')}) %}
{%- endif %}
{# displaying the item #}
{% import _self as knp_menu %}
<li{{ knp_menu.attributes(attributes) }}>
{%- if item.uri is not empty and (not matcher.isCurrent(item) or options.currentAsLink) %}
{{ block('linkElement') }}
{%- else %}
{{ block('spanElement') }}
{%- endif %}
{# render the list of children#}
{%- set childrenClasses = item.childrenAttribute('class') is not empty ? [item.childrenAttribute('class')] : [] %}
{%- set childrenClasses = childrenClasses|merge(['menu_level_' ~ item.level]) %}
{%- set listAttributes = item.childrenAttributes|merge({'class': childrenClasses|join(' ') }) %}
{{ block('list') }}
</li>
{% endif %}
{% endblock %}
{% block linkElement %}
{% import "knp_menu.html.twig" as macros %}
<a href="{{ item.uri }}"{{ macros.attributes(item.linkAttributes) }}>
<span class="icon">{{ item.getExtra('icon') }}</span>
<span class="entry">{{ block('label') }}</span>
</a>
{% endblock %}
{% block spanElement %}{% import _self as knp_menu %}<span{{ knp_menu.attributes(item.labelAttributes) }}>{{ block('label') }}</span>{% endblock %}
You should edit your menu template EspaceClientBundle:Menu:knp_menu.html.twig like:
{% block label %}
{% if options.allow_safe_labels and item.getExtra('safe_label', false) %}
{{ item.label|raw|trans }}
{% else %}
{{ item.label }}
{% endif %}
{% if item.extras.icon is defined %}
<i class="{{ item.extras.icon }}"></i>
{% endif %}
{% endblock %}
I recommend you, to delete the whitespaces between twig tags.
Then, in menu builder:
$menu->addChild('Mes Informations', [
'route' => 'espace_client_mesInformations',
'extras' => ['icon' => 'fa fa-home']
]);

Customising form labels in Symfony

I have a symfony page where I display a form, and I would like to add an
* in the labels of all fields that are required.
So I have my form.php.twig, that looks like this:
{% extends "::layout.php.twig" %}
{% block body %}
<div class="row">
{% use 'form_div_layout.html.twig' with form_label as base_form_label %}
{% block form_label %}
{{ block('base_form_label') }}
{% if required %}
<span class="required" title="This field is required">*</span>
{% endif %}
{% endblock %}
</div>
{% endblock %}
I followed the exact documentation of the symfony cookbook on how to customise labels, which is http://symfony.com/doc/current/cookbook/form/form_customization.html#cookbook-form-theming-methods.
But I keep getting this error
Variable "label" does not exist in form_div_layout.html.twig at line 206
I don't have any label variable in the code of my form.php.twig so I don't see why I get this error. And when I remove the
{{ block('base_form_label') }}
I get
Variable "required" does not exist in ATPlatformBundle:Session:create.php.twig
Can anyone help me on this? I don't see where is my mistake? I am not looking to customise it with css, but to add the * .
I have checked the form_div_layout.html.twig at line 206 and this is what there is
{%- block form_label -%}
{% 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 -%}
{%- if label_format is not empty -%}
{% set label = label_format|replace({
'%name%': name,
'%id%': id,
}) %}
{%- else -%}
{% set label = name|humanize %}
{%- endif -%}
{%- endif -%}
<label{% for attrname, attrvalue in label_attr %} {{ attrname }}="{{ attrvalue }}"{% endfor %}>{{ translation_domain is sameas(false) ? label : label|trans({}, translation_domain) }}</label>
{%- endif -%}
{%- endblock form_label -%}
Thank you in advance.
Did you try defining the label from inside your form builder?! The below field is a required one because unless you mention required => false, your field will be rendered as required.
Something like:
->add('name', 'text', array('label' => '* name'))
With Twig, you should have to test the existence of a variable before using it:
Variable "required" does not exist
{% if required is defined and ... }
You need to test this because you field isn't always required.
if you want further information, you have this page of the documentation:
http://twig.sensiolabs.org/doc/tests/defined.html
Ok, so at the end, I didn't manage to change labels within my form.php.twig (which is the template I use to display my form), but I used another technique.
I created a new file named fields.php.twig, which I put in MyBundle/Resources/views/Form.
At the beginning of my fields.php.twig, I then added
{% extends 'form_div_layout.html.twig' %}
and below it, I added
{% block form_label %}
{{ parent() }}
{% if required %}
<span> * </span>
{% endif %}
{% endblock form_label %}
Then in my form.php.twig, I added
{% form_theme form 'MyBundle:Form:fields.php.twig' %}
{{ form_start(form) }}
Myform here with {{ form_label(form.property) }}
{{ form_errors(form.property) }}
{{ form_widget(form.property) }}
{{ form_end(form) }}
It works perfectly, but I had to create the fields.php.twig.

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.

Twig and autoescaping

I'm learning Symfony2. Currently, I'm trying to render a form label in a twig template. The label includes an html tag, that is not rendered correctly in my twig file.
Here follows the piece of code where the form field is created:
$builder->add('zipcode', 'integer', array(
'label' => '<abbr title="Zone Improvement Plan">CAP</abbr> code',
));
In the twig file I render the field label as follows:
{{ form_label(form.zipcode) }}
I tried the raw, escape, e filters, but the results provided in my html page is always the string
<abbr title="Zone Improvement Plan">CAP</abbr> code
and not the corresponding HTML code!
Any suggestion?
Thanks in advance!
Later I found the solution.
The solution is to disable the autoescape within the label block provided by Symfony at path:
symfony / src / Symfony / Bridge / Twig / Resources / views / Form / form_div_layout.html.twig
So, in your twig file you have to put the following lines outside the form:
{% form_theme form _self %}
{% block generic_label %}
{% spaceless %}
{% if required %}
{% set attr = attr|merge({'class': attr.class|default('') ~ ' required'}) %}
{% endif %}
<label{% for attrname,attrvalue in attr %} {{attrname}}="{{attrvalue}}"{% endfor %}>{% autoescape false %}{{ label|trans }}{% endautoescape %}</label>
{% endspaceless %}
{% endblock %}
From JeanValjean himself :
{% autoescape false %}{{ form.zipcode.vars.label | trans }}{% endautoescape %}
And to generalize this behaviour to your whole app, you can override the form block for labels :
{% block generic_label %}
{% spaceless %}
{% if required %}
{% set attr = attr|merge({'class': attr.class|default('') ~ ' required'}) %}
{% endif %}
<label{% for attrname,attrvalue in attr %} {{attrname}}="{{attrvalue}}"{% endfor %}>
{% autoescape false %}{{ label|trans }}{% endautoescape %}
</label>
{% endspaceless %}
{% endblock %}
To disable the autoespace filter just to render a variable is not the best thing because when you read the code it's not really clear.
So, instead of :
{% autoescape false %}{{ label|trans }}{% endautoescape %}
You can use :
{{ label|trans|raw }}

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