Dynamically call a macro in Twig? - symfony

Is possible to dynamically call a macro in Twig? For example, here is a template and a macro named "group" which builds a button group using buttons array argument. There are also two other macros, save and delete, for building save and delete buttons.
{# Make a group of buttons #}
{% macro group(buttons) %}
{% spaceless %}
{% import "::macros.html.twig" as macros %}
{% set content = '' %}
{% for button in buttons %}
{% set content = content ~ button %}
{% endfor %}
{{ macros.el('div', content, { 'class' : 'btn-group' }) }}
{% endspaceless %}
{% endmacro %}
{# Make a save button #}
{% macro save(attributes, size, image) %}
{% spaceless %}
{{ _self.primary('save'|trans({}, 'buttons'), attributes, size, image) }}
{% endspaceless %}
{% endmacro %}
{# Make a delete button #}
{% macro delete(attributes, size, image) %}
{% spaceless %}
{{ _self.danger('delete'|trans({}, 'buttons'), attributes, size, image) }}
{% endspaceless %}
{% endmacro %}
This works fine passing an array of buttons:
{% import "::buttons.html.twig" as buttons %}
{% set items = [buttons.save, buttons.delete] %}
{{ buttons.group(items) }}
But i'd like to pass macro names to group macro:
{% import "::buttons.html.twig" as buttons %}
{{ buttons.group(['save', 'delete']) }}
and get save and delete macros called automatically. Is this possible and how?

why not just do
{% import "::buttons.html.twig" as buttons %}
{{ buttons.group([buttons.save, buttons.delete]) }}

For those looking for an example of how to implement dynamic macro calling using attribute, checkout https://gist.github.com/tentacode/9728963b9f3a714608f3

Related

symfony's form_errors displays list item instead of just text

[ Symfony 4 ]
I've this template code in Symfony:
{{ form_widget(registrationForm.username, {'attr': {'class': 'form-control'}}) }}
{{ form_errors(registrationForm.username) }}
Instead of just displaying error text, it's generating a list item like this:
<ul><li> Username already exists </li></ul>
How to not have it generate this list item and just get the text?
I guess it is correct behavior, cause you can have multiple errors for one field for example "Username is too short" and "Field Username contains inappropriate characters", but to get only first error you can use:
{{ form_errors(registrationForm.username|first) }}
Or you can customize your form_errors rendering, first create file for form_errors, for example your_form/custom_form_errors.html.twig :
{% block form_errors %}
{% spaceless %}
<div class="error">{{ errors|first }}</div>
{% endspaceless %}
{% endblock %}
And after that include it to your view file:
{% form_theme form 'your_form/custom_form_errors.html.twig' %}
...
{{ form_errors(registrationForm.username) }}
just to extend #Andrii Filenko 's answer. You can modify the output the form_errors or any other form twig function pretty easily. it's called custom theming in Symfony.
Consider this:
// templates/register.html.twig
{% extends "base.html.twig" %}
{% form_theme registrationForm _self %}
{% block form_errors %}
{% spaceless %}
{% if errors|length > 0 %}
<ul class="changed list">
{% for error in errors %}
<li>{{ error.message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endspaceless %}
{% endblock form_errors %}
{% block body %}{% endblock %}
Output:
<ul class="changed list"><li> Username already exists </li></ul>

Twig impossible to invoke a method on a string variable

After updating "twig/twig" to "v2.4.4" I have an error when calling macro function.
Impossible to invoke a method ("widget_prototype") on a string variable (":ERP/timesheets:_form_part.html.twig").
My macro:
{% macro widget_prototype(widget, remove_text) %}
{% if widget.vars.prototype is defined %}
{% set form = widget.vars.prototype %}
{% set name = widget.vars.prototype.vars.name %}
{% else %}
{% set form = widget %}
{% set name = widget.vars.full_name %}
{% endif %}
Calling macro part:
{% include ':ERP/timesheets:_data_content_supply_from_widget.html.twig' with {form:form, name:name} %}
{% endmacro %}
<div id="post_supplies"
data-prototype="{{ _self.widget_prototype(form.supplies, 'remove'|trans({}, 'common'))|escape }}"
style="margin-bottom: 5px">
{% for widget in form.supplies.children %}
{{ _self.widget_prototype(widget, 'remove'|trans({}, 'common')) }}
{% endfor %}
</div>
How to correctly call macro? Tried calling without _self and I have the error: Unknown "widget_prototype" function.
DarkBee's answer is good, but if your macro is in the same twig file that's calling it then you will still need to import it like so:
{% import _self as my_macros %}
{{ my_macros.widget_prototype(...) }}
Seems a bit counter-intuitive but that's how it is.
You need to import the macro, not include it
{% import "my_macro.twig" as my_macro %}
{{ my_macro.function(arg1) }}

SonataAdminBundle templates - list and show fields template content duplication

My list and show fields contain the same content, but due the extending of base_list_field and base_show_field templates I have to create two separate templates.
list/location.html.twig:
{% extends 'SonataAdminBundle:CRUD:base_list_field.html.twig' %}
{% block field %}
{{ object.getCity }}, {{ object.getCountry.getName }}
{% endblock %}
show/location.html.twig:
{% extends 'SonataAdminBundle:CRUD:base_show_field.html.twig' %}
{% block field %}
{{ object.getCity }}, {{ object.getCountry.getName }}
{% endblock %}
As you can see huge part of code is duplicated. Is there a way to check the page I am currently in in twig and then extend certain base template? In this case I would be able to use one file instead of two for same content.
In twig it's possible to extend/include a dynamic template :
{# do test here on which page you are or pass through controller #}
{% if true %}
{% set template = 'SonataAdminBundle:CRUD:base_show_field.html.twig' %}
{% else %}
{% set template = 'SonataAdminBundle:CRUD:base_list_field.html.twig' %
{% endif %}
{% extends template %}
{% block field %}
{{ object.getCity }}, {{ object.getCountry.getName }}
{% endblock %}
(edit) if you don't want the hardcoded if I would pass the template variable through the controller and change the twig template to something like
{% extends template|default('SonataAdminBundle:CRUD:base_show_field.html.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.

How to pass a variable to form_theme?

I want to theme my form so that the field's label show the current locale, something like
Name (en) :
So I would like to rewrite block generic_label like that :
{# form_theme.html.twig #}
{% 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 %}>{{ label|trans }} (app.session.locale)</label>
{% endspaceless %}
{% endblock %}
and import it in my template :
{% form_theme options 'myBundle:Object:form_theme.html.twig' %}
but the app variable is not accessible in the form template.
How can I pass a variable to a form theme ?
In current version of twig (as for 2016) it is possible.
In your template use the following like this:
{{ form_row(form.content, {'testvar' : 'this is test variable'}) }}
Then, in your theme file, just use:
{{testvar}}
of course instead of form.content you will use the field name you need.
Cheers,
Chris
You need to create a form extension in order to get it done. Take a look at
http://toni.uebernickel.info/2011/11/25/how-to-extend-form-fields-in-symfony2.html
to learn how to create the extension.
To have access to session locale, make sure to inject the container. After that you'll be able to get any var value you want.
If the app variable is not available in the form theme it may be a bug. I suggest you create a ticket.
In the meantime you can use the current template as a theme. Something like...
{% form_theme form _self %}
{% block field_label %}
{% set attr = attr|merge({ 'for': id }) %}
{% if required %}
{% set attr = attr|merge({ 'class': attr.class|default('') ~ ' required' }) %}
{% endif %}
<label{% for attrname, attrvalue in attr %} {{ attrname }}="{{ attrvalue }}"{% endfor %}>{{ label|trans }} ({{ app.session.locale }})</label>
{% endblock %}
If you are using Symfony master (2.1) replace app.session.locale with app.request.locale.

Resources