When Symfony finds that your form is invalid, it shows it again but with the addition of errors for each element that failed validation. The errors are basically just unordered lists:
<label>First Name</label>
<ul class='error-list'>
<li>Required.</li>
</ul>
<input type='text' name='first_name'/>
I'm trying to figure out if there is some way to force Symfony to also add custom classes to whatever elements I want when they fail validation. For example add a class='error' to my label or input when the validation fails. That way I can style those elements.
I started looking at form schema decorators but at first glance it doesn't seem like there is a way to do it. But I could be wrong.
Is there anyway to accomplish this?
If your app requires javascript to be enabled, then the easiest and more flexible way to go is to use javascript to through in some classes/attributes dynamically. For example, you could have a script like this
jQuery(document).ready(function($){
if($('.sf_admin_form .error_list').length>0 ){ // style if erro is present
$('.sf_admin_form .error_list').each(function(){
// label of fields with error should be bold and red
$(this).prev('label').css('font-weight', 'bold');
$(this).prev('label').css('color', 'red');
});
}
});
If you do need not count on javascript being enabled, than your choice is use a custom form formatter. The following is an example.
/**
* Class derived from Table formatter, renders customized version if the row has errors.
*
* #package symfony
* #subpackage widget
* #author Paulo R. Ribeiro <paulo#duocriativa.com.br>
*/
class sfWidgetFormSchemaFormatterCustom extends sfWidgetFormSchemaFormatter
{
protected
$rowFormat = "<tr>\n <th>%label%</th>\n <td>%error%%field%%help%%hidden_fields%</td>\n</tr>\n",
// THIS IS NEW
$rowWithErrorsFormat = "<tr class='has-errors'>\n <th class='has-errors'>%label%</th>\n <td class='has-errors'>%error%%field%%help%%hidden_fields%</td>\n</tr>\n",
//
$errorRowFormat = "<tr><td colspan=\"2\">\n%errors%</td></tr>\n",
$helpFormat = '<br />%help%',
$decoratorFormat = "<table>\n %content%</table>";
$errorListFormatInARow = " <ul class=\"error_list\">\n%errors% </ul>\n",
$errorRowFormatInARow = " <li>%error%</li>\n",
$namedErrorRowFormatInARow = " <li>%name%: %error%</li>\n",
public function formatRow($label, $field, $errors = array(), $help = '', $hiddenFields = null)
{
if(count($erros)==0){ // no errors, renders as usual
return strtr($this->getRowFormat(), array(
'%label%' => $label,
'%field%' => $field,
'%error%' => $this->formatErrorsForRow($errors),
'%help%' => $this->formatHelp($help),
'%hidden_fields%' => null === $hiddenFields ? '%hidden_fields%' : $hiddenFields,
));
} else { // has errors, through in some classes
return strtr($this->getRowWithErrorsFormat(), array(
'%label%' => $label,
'%field%' => $field,
'%error%' => $this->formatErrorsForRow($errors),
'%help%' => $this->formatHelp($help),
'%hidden_fields%' => null === $hiddenFields ? '%hidden_fields%' : $hiddenFields,
));
}
}
public function getRowWithErrorsFormat()
{
return $this->rowWithErrorsFormat;
}
}
To enabled the custom formatter for all the forms, use the ProjectConfiguration class to set it up.
// /config/ProjectConfiguration.class.php
class ProjectConfiguration extends sfProjectConfiguration
{
public function setup()
{
/// CODE FOR ENABLING PLUGINS...
// configure your default form formatter
sfWidgetFormSchema::setDefaultFormFormatterName('custom');
}
}
I found another solution using the hasError() method.
<?php if ($form['email']->hasError()) {
echo $form['email']->render( array('class'=>'error') );
} else {
echo $form['email']->render();
} ?>
hasError() apparently checks to see if there are errors for that form element or not.
Instead of applying a condition to every form field you could globally overide the form_label block:
{%- block form_row -%}
<div>
{{- form_label(form) -}}
{{- form_widget(form) -}}
</div>
{%- endblock form_row -%}
{%- 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 -%}
{% if errors|length > 0 %}
{% set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ ' error_class')|trim}) %}
{% endif %}
<label{% for attrname, attrvalue in label_attr %} {{ attrname }}="{{ attrvalue }}"{% endfor %}>{{ label|trans({}, translation_domain) }}</label>
{%- endif -%}
{%- endblock form_label -%}
And in your config:
twig:
form:
resources:
- 'Form/fields.html.twig'
Related
I want to add href link in form error message, and this link is not static.I`m try more way, but not has pertect way.I need help
index.html.twig:
{% extends ... %}
{% form_theme form 'Form/bootstrap_3_horizontal_errors_layout.html.twig' %}
bootstrap_3_horizontal_errors_layout.html.twig:
{% extends 'bootstrap_3_horizontal_layout.html.twig' %}
{% block form_errors -%}
{% if errors|length > 0 -%}
{% if form.parent %}<span class="help-block">{% else %}<div class="alert alert-danger">{% endif %}
<ul class="list-unstyled">
{%- for error in errors -%}
<li>
<span class="glyphicon glyphicon-exclamation-sign"></span> {{ error.message }}
delete
</li>
{%- endfor -%}
</ul>
{% if form.parent %}</span>{% else %}</div>{% endif %}
{%- endif %}
{%- endblock form_errors %}
validator.php:
public function validate($cooperation, Constraint $constraint)
{
$couponCodeOr400 = $cooperation->getCouponCodeOr400();
$qb = $this->repo->createQueryBuilder('c')
->select('c.id')
->where('c.couponCodeOr400 = :couponCodeOr400')
->setParameters(compact('couponCodeOr400'));
$id = $cooperation->getId();
if (null !== $id) {
$qb->andWhere('c.id != :id')->setParameter('id', $id);
}
$alreadyId = $qb->getQuery()->getOneOrNullResult();
if ($alreadyId) {
$this->context->buildViolation(sprintf('coupon %s is already exist', $couponCodeOr400))
->addViolation();
}
}
In controller, I need this id for "route" parameters on twig, How to pass this value back.
controller:
public function newAction(Request $request)
{
$form = $this->createForm(CooperationType::class);
$form->handleRequest($request);
if ($form->isValid()) {
$cooperation = $form->getData();
$em = $this->get('doctrine.orm.entity_manager');
$em->persist($cooperation);
$em->flush();
return $this->redirectToRoute('admin_cooperation_edit', ['id' => $cooperation->getId()]);
}
return $this->render('admin/cooperation/new.html.twig', [
'form' => $form->createView(),
]);
}
Hope you has better way add link in form error
Symfony 2.8
I'm trying to override some blocks of bootstrap themes for forms.
Particularly interested the block "form_label_class" in the theme "bootstrap_3_horizontal_layout.html.twig":
{% block form_label_class -%}
col-sm-2
{%- endblock form_label_class %}
I created the file my_form.html.twig and inherited it from "bootstrap_3_horizontal_layout.html.twig":
{% use "bootstrap_3_horizontal_layout.html.twig" %}
{% block form_start -%}
...
{% endblock form_start -%}
{% block form_label_class -%}
{{ width_test }}
{%- endblock form_label_class %}
Also added the definition of "width_test" in the form class:
....
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['width_test'] = $options['width_test'];
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('width_test', 3);
}
The problem: as a result of getting the error:
Variable "width_test" does not exist in form/my_form.html.twig
Importantly, "width_test" is not defined only in the block "form_label_class", but no problems to display it for example in the block "form_start"
Symfony renders an entity field type like a choice dropdown - a select, basically. However, the CSS framework that I'm using defines a sort of 'select' as a ul and li as the options. The Custom Field Type documentation gives no help on this scenario.
I'm converting my code from manual HTML rendering of the form dropdown to symfony form's version using twig and form_widget(). However, I want a ul and li instead of a select.
The manual way of creating my dropdown is:
<ul class='dropdown-menu'>
{% for locator in locators %}
<li>
<a href="#" data-id="{{locator.getId() }}">
{{ locator.getName() }}
</a>
</li>
{% endfor %}
</ul>
That's how I would render my dropdown manually before using symfony forms. It looks like this:
I like it. I think it looks awesome. Now, if I'm using Symfony forms, I can just use this instead:
{{ form_start(form) }}
{{ form_widget(form.locator) }} {# This is my locator dropdown #}
{{ form_widget(form.target) }} {# Ignore this #}
{{ form_end(form) }}
The problem is that this renders this instead:
I can't add my custom CSS here because this is rendered as a select instead of an unordered list and lis.
In case it may help, here's my form type being built:
/**
* {#inheritDoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('target')
->add('locator', 'entity', [
'class' => 'Application\Model\Entity\Locator',
'query_builder' => function(EntityRepository $repo) {
return $repo->createQueryBuilder('e');
},
'empty_value' => 'Locator'
])
->add('save', 'submit', ['label' => 'Save']);
$builder->setAction($this->urlGenerator->generate('page_create_element', [
'suiteId' => $options['suiteId'], 'pageId' => $options['pageId']
]))->setMethod('POST');
}
The Question: Is there any way I can have the form commands above auto-generate my ul / li requirement instead of selects, or do I have to render this manually instead and ignore the symfony forms component for this?
Thanks to some of the posters above, there was some information from Form Theming, but it wasn't exactly enough to go along with so I had to do a little bit of digging on github.
According to the documentation, Symfony uses twig templates to render the relevant bits of a form and it's containing elements. These are just {% block %}s in twig. So the first step was to find where a select button is rendered within the symfony codebase.
Form Theming
Firstly, you create your own theme block in it's own twig file and you apply this theme to your form with the following code:
{% form_theme my_form_name 'form/file_to_overridewith.html.twig %}
So if I had overridden {% block form_row %} in the file above, then when I called {{ form_row(form) }} it would use my block instead of Symfony's default block.
Important: You don't have to override everything. Just override the things you want to change and Symfony will fall back to it's own block if it doesn't find one in your theme.
The Sourcecode
On github I found the source code for Symfony's "choice widget". It's a little complex but if you follow it through and experiment a little bit you'll see where it goes.
Within the choice_widget_collapsed block, I changed the select to uls and options to lis. Here's the theme file I created, note the minor differences described above:
{# Symfony renders a 'choice' or 'entity' field as a select dropdown - this changes it to ul/li's for our own CSS #}
{%- block choice_widget_collapsed -%}
{%- if required and empty_value is none and not empty_value_in_choices and not multiple -%}
{% set required = false %}
{%- endif -%}
<ul {{ block('widget_attributes') }}{% if multiple %} multiple="multiple"{% endif %}>
{%- if preferred_choices|length > 0 -%}
{% set options = preferred_choices %}
{{- block('choice_widget_options') -}}
{%- if choices|length > 0 and separator is not none -%}
<li disabled="disabled">{{ separator }}</li>
{%- endif -%}
{%- endif -%}
{%- set options = choices -%}
{{- block('choice_widget_options') -}}
</ul>
{%- endblock choice_widget_collapsed -%}
{%- block choice_widget_options -%}
{% for group_label, choice in options %}
{%- if choice is iterable -%}
<optgroup label="{{ group_label|trans({}, translation_domain) }}">
{% set options = choice %}
{{- block('choice_widget_options') -}}
</optgroup>
{%- else -%}
<li value="{{ choice.value }}"{% if choice is selectedchoice(value) %} selected="selected"{% endif %}>{{ choice.label|trans({}, translation_domain) }}</li>
{%- endif -%}
{% endfor %}
{%- endblock choice_widget_options -%}
Rendering
Now I can render my form with the following:
{{ form_widget(form.locator, {'attr': {'class': 'dropdown-menu'}}) }}
This uses my theme for the choice dropdown which contains ul and li tags instead of select and option ones. Pretty simple once you know where to look for the original code! The rendered HTML:
<ul id="elementtype_locator" name="elementtype[locator]" required="required" class="dropdown-menu">
<li value="1">id</li>
<li value="2">name</li>
<li value="3">xpath</li>
</ul>
I also had to remove one of the lines that put 'Locator' at the top of the dropdown as there were four dropdown choices (including the empty_data one) instead of three.
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);
}
}
OP note: it was simply that li elements aren't passed through in forms which I forgot about.
For context please see my Previous Question on how I required the ability to change select and option tags to ul an li tags when rendering a dropdown with Symfony 2 forms as they look and work well with my CSS framework of choice.
As a result of changing how my form element is rendered, it seems that the data is no longer coming through after submitting.
I am overriding the {% choice_widget_* %} parts of the choice field in order to change these to ul and lis. Here is my change:
{%- block choice_widget_collapsed -%}
{%- if required and empty_value is none and not empty_value_in_choices and not multiple -%}
{% set required = false %}
{%- endif -%}
<ul {{ block('widget_attributes') }}>
{%- if preferred_choices|length > 0 -%}
{% set options = preferred_choices %}
{{- block('choice_widget_options') -}}
{%- if choices|length > 0 and separator is not none -%}
<li disabled="disabled">{{ separator }}</li>
{%- endif -%}
{%- endif -%}
{%- set options = choices -%}
{{- block('choice_widget_options') -}}
</ul>
{%- endblock choice_widget_collapsed -%}
{%- block choice_widget_options -%}
{% for group_label, choice in options %}
{%- if choice is iterable -%}
<optgroup label="{{ group_label|trans({}, translation_domain) }}">
{% set options = choice %}
{{- block('choice_widget_options') -}}
</optgroup>
{%- else -%}
<li value="{{ choice.value }}"{% if choice is selectedchoice(value) %} selected="selected"{% endif %}>{{ choice.label|trans({}, translation_domain) }}</li>
{%- endif -%}
{% endfor %}
{%- endblock choice_widget_options -%}
I found and modified this code from Symfony 2 github source, and it allowed me to render my form as such:
The problem: When submitting this form, the "target" field comes through fine, but my overriding seems to have corrupted how the data is retrieved from the dropdown and my debugging shows empty data fields.
Here's the break line in setDefaultOptions() within my type:
Here's the form data. Please note how the modelData does indeed exist for "target', which is a standard input field, and how the modelData does not exist for the locator dropdown (highlighted):
So the data is coming through as null because of how I've changed how the form is rendered, but what can I do about this?
Can I place some sort of adapter or data transformer to handle this change? What needs to be done? I can imagine this happening with anyone who wants to theme their own form and remove some of the annoying divs that are injected by symfony during rendering. What can I do?
<li></li> elements are not form elements (and thus won't get submitted with the form).
You would need to add a form submit handler (using javascript) that turns the li elements back into some sort of form elements.
Ideally you would transform them back to the <option></option> elements so you don't need a data-transformer on the server.
jQuery boilerplate code:
$(document).ready(function() {
$("#myForm1").submit(function() {
//Gets fired when the form is about to be submitted, but before the actual request
$("#myForm1").append("<optgroup name=.......");
$("ul[name=\"somebundle_formname_fieldname\"]").children("li").each(function () {
$("#myForm1").find("<optgroup name=.......").append("<option value=" + $(this).attr("value") +">bla</option>");
});
});
});
Vanilla JS (IE9+, Chrome, Firefox) boilerplate code:
document.addEventListener("DOMContentLoaded", function() {
var myForm1 = document.getElementById("myForm1");
myForm1.addEventListener('submit', function(e) {
//Gets fired when the form is about to be submitted, but before the actual request
var tmpOptGroup = document.createElement("optgroup");
tmpOptGroup.name = "somebundle_formname_fieldname";
myForm1.appendChild(tmpOptGroup);
var liElements = document.querySelectorAll("ul[name=\"somebundle_formname_fieldname\"] li");
for(var i=0;i<liElements.length;i++) {
var currentLiElement = liElements[i];
//add option, see jquery above
}
});
});