A browser validates a form on its own before it fires the submit event. The
widgets Symfony renders carry the attributes it validates from: required on
every field whose required option is left at its default, type="email" for
an EmailType, type="number" for the numeric types, and whatever min,
max, step or pattern the field puts in its attr option.
Without the integration described here the two validations run side by side and do not know about each other:
- a field the browser refuses stops the
submitevent, so this bundle never gets to render its error list and the user only sees a native bubble on the first offending field; - a field this bundle refuses is
:validfor the browser, andform.checkValidity()answerstruefor a form the bundle will not submit.
Turning the integration on makes this bundle own the reporting:
# config/packages/svaroh_js_form_validator.yaml
svaroh_js_form_validator:
html5_validation: trueThe flag is exported with the rest of the configuration, so the template should render it before the form models are initialized:
{{ js_validator_config() }}
{{ init_js_validation() }}A template that renders the configuration further down the document still ends up with the integration on: the models re-read the flag when the document is ready and again when a form is submitted.
Four things change once it is on.
Every form this bundle initializes receives the novalidate attribute, which
turns off the interactive validation the browser performs on submit. The
submit event is fired again for forms the browser used to block, and the error
list of this bundle is rendered where a bubble used to appear.
novalidate only suppresses that step. :invalid styling, willValidate,
validity and form.checkValidity() keep working.
Because the browser no longer blocks the submit, this bundle reads its verdict
itself. Whenever the validation of an element runs, the validity of its widget
is inspected, and a failing widget contributes the validationMessage of the
browser to the same <ul class="form-errors"> list the constraint messages are
rendered in. Everything the Constraint Validation API reports is covered:
badInput, valueMissing, typeMismatch, patternMismatch, tooLong,
tooShort, rangeUnderflow, rangeOverflow and stepMismatch.
A required attribute and a NotBlank constraint describe the same failure, so
a value the browser only reports as missing is left to the constraint. The
message of the constraint wins there, because it is the translated one and it is
the message the server would produce.
Every other diagnosis of the browser is a different failure and is reported even
when a constraint already spoke about the element. An <input type="number">
filled with letters reads back as an empty value, so a NotBlank constraint
fires on it; the browser is still heard, and the user is told that the value is
not a number instead of only being told that it is empty.
An expanded choice renders one widget per choice and each of them carries the
same required attribute, so the browser refuses every one of them for the one
missing value. The group is reported once, on its first widget.
The native message comes from the browser. It is written in the language of the
browser, not in the locale of the application, and it cannot be changed with the
invalid_message option of the field.
After an element is validated, its first error is written to the widget with
setCustomValidity(), and a valid element has that state cleared. A field this
bundle refuses therefore matches :invalid, reports the message through
validationMessage, and makes form.checkValidity() answer false:
input:invalid {
border-color: #b94a48;
}The mirrored state is exactly as fresh as the last validation run. This bundle validates on submit, and on whatever else the application asks it to validate on (see Run validation on custom event); it does not attach a listener to every field, so the state does not follow the user while typing unless the application asks for it.
Errors pushed onto an element from elsewhere are mirrored the same way, so the
answer of the entity uniqueness check reaches :invalid too. Only
widgets carry native validity, so an error routed to a form - a constraint on a
field with error_bubbling, which moves the error up to the closest ancestor
that does not bubble, or an errorPath pointing at a form - is rendered in the
error list without a widget to mirror it onto.
novalidate is an attribute of the form, so it takes the enforcement of the
browser away from every widget at once, including the ones this bundle has no
model for: a field excluded with js_validation => false, a widget switched off
with disabled, a control a form theme rendered outside of the model.
Those widgets are read once more when the form is submitted, and whatever the browser still refuses and no constraint reported is added to the error list and refuses the submit, the way the browser used to refuse it. Switching the validation of a field off therefore keeps meaning "do not apply the constraints of the model to it", not "let it through".
- This bundle does not generate HTML5 attributes out of Symfony constraints. An
Assert\Rangedoes not becomeminandmax, anAssert\Regexdoes not become apattern. Only the attributes the form theme already renders are read. - No constraint is skipped because the browser already covers it. Both validations still run; the integration only decides which of the two messages a shared failure is reported with.
- The flag is global. There is no per-form or per-field way to enable it, and
novalidateis applied to every form this bundle initializes.