I would like add a CSS class on a form_widget() when errors (specific to field) happen.
For now, I have this code :
{{ form_widget(form.firstName, {'attr': {'class': 'text-input'}}) }}
It generates this HTML code :
<input type="text" id="userbundle_member_firstName" name="userbundle_member[firstName]" required="required" class="text-input" />
It's good but I want when error(s) happen (exemple the value is too short), a css class will added. So, I want to have something like that :
<input type="text" id="userbundle_member_firstName" name="userbundle_member[firstName]" required="required" class="text-input error-input" />
I tried searching solution on the doc (especially here), but I have not been able to do what I want.
Thank you for your help :D !
Tell Twig about your custom form's templates:
app/config/config.yml
twig:
debug: %kernel.debug%
strict_variables: %kernel.debug%
form:
resources:
- 'YourBundle::form.html.twig'
And override them (check vendor/symfony/symfony/src/Symfony/Bridge/Twig/Resources/views/Form):
src/YourBundle/Resources/views/form.html.twig
{% block form_widget_simple %}
{% if form.vars.errors|length > 0 %}
{% set class = attr.class is defined ? attr.class ~ ' error' : 'error' %}
{% set attr = attr|merge({'class': class}) %}
{% endif %}
{% spaceless %}
{% set type = type|default('text') %}
<input type="{{ type }}" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %}/>
{% endspaceless %}
{% endblock form_widget_simple %}
UPDATE
The method described above will change the rendering of all the form fragments, if you only need to change some form then you can do it locally, just read the docs (https://symfony.com/doc/current/book/forms.html#form-theming)
Related
Symfony 3.3
I have a form of my Voyage entity
Voyage entity has a Collection in it, named cities, collection of entity City.
And so do the form with the Collection named cities.
The user first use the form and create an instance of Voyage and add some cities to it, I managed to customise the prototype and render it via javascript when the user click "add city" button.
The form is rendered this way for the interesting part (cleaned version without html):
{% extends "#User/layout.html.twig" %}
{% form_theme form.cities '#Prototype/city.html.twig' %}
{% block content %}
{{ form_start(form) }}
{{ form_errors(form) }}
{{ form_row(form.cities) }}
{{ form_rest(form) }}
{{ form_end(form) }}
{% endblock content %}
The theme for the 'form.cities' :
{% block collection_widget %}
{% import '#Prototype/prototype.city.twig' as proto %}
{% spaceless %}
<div class="collection">
{% if prototype is defined %}
{% set attr = attr|merge({'data-prototype': proto.city(prototype)|escape }) %}
{% endif %}
<div {{ block('widget_container_attributes') }}></div>
<div id="container-cities">
{# Here I will add the cities via javascript when user add one #}
</div>
</div>
{% endspaceless %}
{% endblock collection_widget %}
The macro file used in this theme and imported as proto :
{% macro city(widget, id, name, zip) %}
{% spaceless %}
<div
class="added-city border-gray"
data-id="{{id|default('__id__')}}"
id="{{name|default('__name__')}}">
{{name|default('__city_name__')}} ({{zip|default('__zip__')}})
{{ form_errors(widget) }}
{{ form_widget(widget) }}
</div>
{% endspaceless %}
{% endmacro %}
My general problem : When the user wants to edit its instance of Voyage, it already has some cities in it. How can I render them ? How can I access the cities variable from within the theme.
My partial solution : I wanted to extract the 'container-cities' block from the theme file to the rendered html where the form is initialy rendered and where I can access the variables and do this :
{% import '#Prototype/prototype.city.twig' as proto %}
{% for city in form.cities %}
{{ proto.city(city, city.name) }}
{% endfor %}
But it give me this error :
Neither the property "name" nor one of the methods "name()", "getname()"/"isname()"/"hasname()" or "__call()" exist and have public access in class "Symfony\Component\Form\FormView".
(It doesn't fail on city.id probably because of some other field named id)
My question :
How can I access the cities within the theme and render them with my macro ?
Or
How to access to the cities items where I render the form, because the form.cities doesn't seem to be the actual City entity Collection, and get rid of that error ?
Thanks
I finally solved it, it was that simple :
I just used this in the form theme :
{% for city in form %}
{{ proto.city(city, city.vars.value.id, city.vars.value.name, city.vars.value.zip) }}
{% endfor %}
I have Symfony3 app and I am making a simple form the code in the twig is as follows
{{ form_start(edit_form) }}
{{ form_widget(edit_form) }}
<input type="submit" value="Edit" />
{{ form_end(edit_form) }}
Pretty simple. What this code creates is a form and each form field is within it's own <div> which is fine, but if the type is date here is what the generated html looks like
<div>
<label class="required">Term</label>
<div id="appbundle_project_term">
<select id="appbundle_project_term_year" name="appbundle_project[term][year]"></select>
<select id="appbundle_project_term_year" name="appbundle_project[term][month]"></select>
<select id="appbundle_project_term_year" name="appbundle_project[term][day]"></select>
</div>
</div>
What bugs me is the inner div created for the date type field. Is there a way in the FormBuilder to keep the type date but remove this inner div without using javascript to handle it or in the twig template. Simply to say - "inner tag => span".
This is pretty generic question as I am looking for a way to usually change the auto generated tags, but if needed here is how this form field is created in form builder
add('term',DateType::class, array(
'widget' => 'choice',
'label'=>"Term",
'data'=>$project->getTerm()
))
You can override form rendering, there are few ways.
The simplest one is overriding form theme widget block (in this case date_widget) and setting form_theme to _self.
Basic example:
{% form_theme form _self %}
{% block date_widget %}
<span>
{% if widget == 'single_text' %}
{{ block('form_widget_simple') }}
{% else %}
{# rendering 3 fields for year, month and day #}
{{ form_widget(form.year) }}
{{ form_widget(form.month) }}
{{ form_widget(form.day) }}
{% endif %}
</span>
{% endblock %}
{% block content %}
{# ... form rendering #}
{{ form_row(form.someDateField) }}
{% endblock %}
I have created a custom form type and contraint in Symfony.
The constraint is attached to the form type like this:
->add('customField', 'customField', array(
'required' =>
'mapped' => false,
'constraints' => array(new CustomField()),
))
where CustomField is the constraint class.
The constraint validator's validate() method looks like this:
public function validate($value, Constraint $constraint)
{
//I know this will always fail, but it's just for illustration purposes
$this->context->addViolation($constraint->message);
}
I have changed the form's default template like this:
{% block form_row -%}
<div class="form-group">
{{- form_widget(form) -}}
{{- form_errors(form) -}}
</div>
{%- endblock form_row %}
{% block customField_widget %}
{% spaceless %}
<!-- actually different but you get the idea -->
<input type="text" name="customField" id="customField" />
{% endspaceless %}
{% endblock %}
{% block form_errors -%}
{% if errors|length > 0 -%}
{%- for error in errors -%}
<small class="help-block">
{{ error.message }}
</small>
{%- endfor -%}
{%- endif %}
{%- endblock form_errors %}
And in the template where the form is displayed, I've added some code to display the errors attached to the whole form rather than individual field errors:
{{ form_start(formAdd) }}
{% if formAdd.vars.valid is same as(false) -%}
<div class="alert alert-danger">
<strong>Errors!</strong> Please correct the errors indicated below.
{% if formAdd.vars.errors %}
<ul>
{% for error in formAdd.vars.errors %}
<li>
{{ error.getMessage() }}
</li>
{% endfor %}
</ul>
{% endif %}
</div>
{%- endif %}
...
The problem with all this, the validator of this particular field, is attaching the constraint violation to the form object and not to the customField form type. This causes the error to be finally displayed with the form's general errors instead of being displayed as a field error.
Now, this is not the only custom form type and validator I added but it's the only one that displays this behavior, without me being able to identify the difference between this one and the rest. Can you spot what is wrong here?
I have sorted this out myself. It has nothing to do with the contraint or the validator. The issue was with the custom form type (which I haven't described in my question). The problem was that this form type had "form" as a parent which is a compound type. This means that by default (according to the docs), the error bubbling is also true and that, in turn, means that "any errors for that field will be attached to the main form, not to the specific field".
You have to specify path in your validator:
$this->context
->buildViolation($constraint->message)
->atPath('customField')
->addViolation();
You have to set 'error_bubbling' to false in your custom form type
class CustomFieldType extends AbstractType
{
public function getName()
{
return 'customField';
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('error_bubbling', false);
}
}
Default radio widget creates a following structure:
<label>...</label>
<div id="...">
<div class="clearfix prettyradio labelright blue">
<input type="radio" id="..._0" name="..." value="..." style="display: none;">
...
</div>
I found the radio_widget block, but it contains only an input itself. So I can customize there only this part:
<input type="radio" id="..._0" name="..." value="1" style="display: none;">
But I can't understand how to change whole the structure of radio choice field?
Also, does anybody knows, why symfony adds display:none to the input?
Thanks.
if you're using Radio Field Type, you can customize only the input part of the radio_widget block by calling form_widget(form.yourField), all it displays is,
{% block radio_widget %}
{% spaceless %}
<input type="radio" {{ block('widget_attributes') }}{% if value is defined %} value="{{ value }}"{% endif %}{% if checked %} checked="checked"{% endif %} />
{% endspaceless %}
{% endblock radio_widget %}
But if you're using Choice Field Type to display Radio Fields (expanded => true and multiple => false). You'll then have to override the choice_widget block, which call for each child element the radio_widget block surrounded by a global div
How did you get the "display:none"? because there's no style such this in the default block.
If you specifically want to override the way an individual radio field is rendered - i.e. how each input field in the group is rendered - use this formula for the block name:
_<form name>_<field name>_entry_widget
Note this bit: _entry
If you're using an expanded choice field ..._entry_row and ..._entry_label won't work because they aren't used for the individual choices - well for radio buttons at least.
More generally you can find out a lot about which block Symfony intends to use during the next call to {{ form_widget(form) }} function call using this code:
{% for b in form.vars.block_prefixes %}
{{ dump(b) }}
{% endfor %}
or you might want to look at child in some situations:
{% for b in child.vars.block_prefixes %}
{{ dump(b) }}
{% endfor %}
I am trying to implement something like this:
<div>
<input type="checkbox" name="checkbox" id="checkbox_id" />
<label for="checkbox_id">I agree to the Terms of Service</label>
</div>
The closest I've come to implement this is through:
<div>
{{ form_widget(form.agreeWithTos) }}
<label for="{{ form.agreeWithTos.vars.id }}">I agree to the Terms of Service</label>
</div>
Is there a better way? Having to specify {{ form.agreeWithTos.vars.id }} is inelegant. :)
Solved this problem using the following code in my form-theme:
{# ---- form-theme.html.twig #}
{% block checkbox_row %}
{% spaceless %}
<div>
{{ form_errors(form) }}
<label class="checkbox" for="{{ form.vars.id }}">
{{ form_widget(form) }}
{{ label|default(form_label(form)) | raw }}
</label>
</div>
{% endspaceless %}
{% endblock %}
in your Form-Template you can then use:
{% form_theme form '::form-theme.html.twig' %}
{{form_row(form.termsOfServiceAccepted, {
'label' : 'I have read and agree to the Terms and conditions'
})
}}
this way, the block from the form-theme would apply to any checkbox on the page. If you need to also use the default-theme, you can add a parameter to enable special-rendering:
{# ---- form-theme.html.twig #}
{% block checkbox_row %}
{% spaceless %}
{% if not useTosStyle %}
{{ parent() }}
{% else %}
{# ... special rendering ... #}
{% endif %}
{% endspaceless %}
{% endblock %}
which would be used like this:
{% form_theme form '::form-theme.html.twig' %}
{{form_row(form.termsOfServiceAccepted, {
'useTosStyle' : true,
'label' : 'I have read and agree to the Terms and conditions'
})
}}
Thanks to a recent commit to Symfony, you can use label_html from Symfony 5.1 onward:
{{ form_label(
form.privacy,
'I accept the privacy terms.',
{
'label_html': true,
},
) }}
I've been beating my head over this then had a eureka moment. The easiest way to do this–BY FAR–is to create a Twig extension.
Here's my Twig code:
{# twig example #}
{% block form_label %}
{% set label = parent() %}
{{ label|unescape|raw }}
{% endblock %}
and PHP:
<?php
new Twig_SimpleFilter('unescape', function($value) {
return html_entity_decode($value);
});
A couple notes:
This unescapes all previously escaped code. You should definitely re-escape afterwards as necessary.
This requires an extends tag for your target form theme in your own custom form theme which you can use in your subsequent forms. Put the twig code in a custom form theme and replace references to your other form theme/themes with this one.
Overall this is the fewest lines of code for the biggest and best outcome that I've been able to find. It's also ultra-portable and DRY: You can extend any form theme and the label will change without you changing the rest of your code.
Another very simple approach is to override the form theme directly in the template which renders the form. Using {% form_theme form _self %} it is as simple as this:
{% form_theme form _self %}
{% block form_label %}
{{ label | raw }}
{% endblock %}
{{ form_start(form) }}
See the corresponding section in the docs:
https://symfony.com/doc/current/form/form_customization.html#method-1-inside-the-same-template-as-the-form
The easiest way to customize the [...] block is to customize it directly in the template that's actually rendering the form.
By using the special {% form_theme form _self %} tag, Twig looks inside the same template for any overridden form blocks. [...]
The disadvantage of this method is that the customized form block can't be reused when rendering other forms in other templates. In other words, this method is most useful when making form customizations that are specific to a single form in your application. If you want to reuse a form customization across several (or all) forms in your application, read on to the next section.
Another approach is to use a simple Twig replacement:
{% set formHtml %}
{{ form(oForm) }}
{% endset %}
{{ formHtml|replace({'[link]': '', '[/link]': ''})|raw }}
In your form, you have something like this in order to make it work:
$formBuilder->add('conditions', CheckboxType::class, [
'label' => 'Yes, I agree with the [link]terms and conditions[/link].'
]
);
Of course, you may change [link] to anything else. Please note that you do not use HTML tags ;-)
I think you are looking for form theming. That way you are able to style each part of form, in an independent file, anyway you want and then just render it in "elegant" way, row by row with {{ form_row(form) }} or simply with {{ form_widget(form) }}. It's really up to you how you set it up.
Symfony 4.2
TWIG:
{% block main %}
....
{% form_theme form _self %}
...
{{ form_row(form.policy, {'label': 'security.newPassword.policy'|trans({"%policyLink%":policyLink, "%termsLink%":termsLink})}) }}
...
{% endblock %}
{% block checkbox_radio_label %}
<label{% with { attr: label_attr } %}{{ block('attributes') }}{% endwith %}>
{{- widget|raw }} {{ label|unescape|raw }}
</label>
{% endblock checkbox_radio_label %}
PHP:
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
class AppExtension extends AbstractExtension
{
public function getFilters()
{
return [
new TwigFilter('unescape', function ($value) {
return html_entity_decode($value);
}),
];
}
}
So form theming is pretty complicated. The easiest thing I've found is to just suppress the field's label ('label'=> false in Symfony form class) and then just add the html label in the twig html.
You could leverage form theming in another way: you could move the <label> tag outside the form_label() function.
Create a custom form theme, and for checkboxes only move the <label> tag outside the form_label function:
{% block checkbox_row %}
<label>{{ form_label(form) }}</label>
{{ form_errors(form) }}
{{ form_widget(form) }}
{% endblock checkbox_row %}
{% block checkbox_label %}
{{ label }}
{% endblock checkbox_label %}
Now, in your teplate, override the label of your checkbox, and thus effectively inject HTML into the label function:
{% form_theme form 'yourtheme.html.twig' _self %}
{% block _your_TOS_checkbox_label %}
I agree with terms and conditions
{% endblock _your_TOS_checkbox_label %}